Skip to content

Commit 0cdf83d

Browse files
committed
feat(cpal): add audio input stream and cpal integration
- Add SckAudioInputStream for cpal-style callback-based audio capture - Add SckAudioConfig for configuring audio capture parameters - Add AudioRingBuffer for thread-safe audio transfer between SCK and cpal - Add create_output_callback helper for easy cpal output integration - Add SckAudioCallbackInfo for callback metadata - Update example to use new simplified API - Add ring buffer tests for write/read, wrap-around, and edge cases
1 parent c0eab6d commit 0cdf83d

3 files changed

Lines changed: 621 additions & 117 deletions

File tree

examples/17_cpal_audio.rs

Lines changed: 28 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -6,81 +6,11 @@
66
//!
77
//! Note: Requires screen recording permission and audio output device.
88
9-
use screencapturekit::cpal_adapter::{AudioFormat, CpalAudioExt};
9+
use screencapturekit::cpal_adapter::{create_output_callback, SckAudioInputStream};
1010
use screencapturekit::prelude::*;
11-
use std::sync::{Arc, Mutex};
1211

1312
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
1413

15-
/// Ring buffer for audio samples
16-
struct AudioRingBuffer {
17-
buffer: Vec<f32>,
18-
write_pos: usize,
19-
read_pos: usize,
20-
capacity: usize,
21-
}
22-
23-
impl AudioRingBuffer {
24-
fn new(capacity: usize) -> Self {
25-
Self {
26-
buffer: vec![0.0; capacity],
27-
write_pos: 0,
28-
read_pos: 0,
29-
capacity,
30-
}
31-
}
32-
33-
fn write(&mut self, samples: &[f32]) {
34-
for &sample in samples {
35-
self.buffer[self.write_pos] = sample;
36-
self.write_pos = (self.write_pos + 1) % self.capacity;
37-
}
38-
}
39-
40-
fn read(&mut self, output: &mut [f32]) {
41-
for sample in output.iter_mut() {
42-
*sample = self.buffer[self.read_pos];
43-
self.read_pos = (self.read_pos + 1) % self.capacity;
44-
}
45-
}
46-
}
47-
48-
struct AudioHandler {
49-
ring_buffer: Arc<Mutex<AudioRingBuffer>>,
50-
format_detected: Arc<Mutex<Option<AudioFormat>>>,
51-
}
52-
53-
impl SCStreamOutputTrait for AudioHandler {
54-
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, of_type: SCStreamOutputType) {
55-
if of_type != SCStreamOutputType::Audio {
56-
return;
57-
}
58-
59-
// Detect audio format on first sample
60-
{
61-
let mut format = self.format_detected.lock().unwrap();
62-
if format.is_none() {
63-
if let Some(f) = AudioFormat::from_sample_buffer(&sample) {
64-
println!(
65-
"🔊 Audio format detected: {}Hz, {} channels, {} bits, float={}",
66-
f.sample_rate, f.channels, f.bits_per_sample, f.is_float
67-
);
68-
*format = Some(f);
69-
}
70-
}
71-
}
72-
73-
// Copy audio samples to ring buffer
74-
if let Some(samples) = sample.audio_f32_samples() {
75-
let slice = samples.as_f32_slice();
76-
if !slice.is_empty() {
77-
let mut rb = self.ring_buffer.lock().unwrap();
78-
rb.write(slice);
79-
}
80-
}
81-
}
82-
}
83-
8414
fn main() -> Result<(), Box<dyn std::error::Error>> {
8515
println!("🎵 cpal Audio Capture Example");
8616
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
@@ -103,37 +33,31 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
10333
.exclude_windows(&[])
10434
.build();
10535

106-
// Configure stream with audio capture
107-
let config = SCStreamConfiguration::new()
108-
.with_width(1920)
109-
.with_height(1080)
110-
.with_captures_audio(true)
111-
.with_sample_rate(48000)
112-
.with_channel_count(2);
113-
114-
// Setup ring buffer for audio transfer
115-
let ring_buffer = Arc::new(Mutex::new(AudioRingBuffer::new(48000 * 2 * 2))); // 2 seconds buffer
116-
let format_detected = Arc::new(Mutex::new(None));
117-
118-
let handler = AudioHandler {
119-
ring_buffer: Arc::clone(&ring_buffer),
120-
format_detected: Arc::clone(&format_detected),
121-
};
122-
123-
// Start capture
124-
let mut stream = SCStream::new(&filter, &config);
125-
stream.add_output_handler(handler, SCStreamOutputType::Audio);
126-
stream.start_capture()?;
127-
128-
println!("🎬 Capture started, waiting for audio format detection...");
129-
130-
// Wait for audio format detection
131-
let audio_format = loop {
132-
std::thread::sleep(std::time::Duration::from_millis(100));
133-
if let Some(format) = format_detected.lock().unwrap().clone() {
134-
break format;
36+
// Create SCK audio input stream
37+
let mut input = SckAudioInputStream::new(&filter)?;
38+
let buffer = input.ring_buffer().clone();
39+
40+
println!(
41+
"🔊 Audio config: {}Hz, {} channels",
42+
input.sample_rate(),
43+
input.channels()
44+
);
45+
46+
// Start capture - the callback receives samples (we also log)
47+
input.start(|samples, info| {
48+
static mut SAMPLE_COUNT: usize = 0;
49+
unsafe {
50+
SAMPLE_COUNT += samples.len();
51+
if SAMPLE_COUNT % (info.sample_rate as usize * 2) < samples.len() {
52+
println!(
53+
"📥 Captured {} samples total",
54+
SAMPLE_COUNT / info.channels as usize
55+
);
56+
}
13557
}
136-
};
58+
})?;
59+
60+
println!("🎬 Capture started");
13761

13862
// Setup cpal output
13963
let host = cpal::default_host();
@@ -143,15 +67,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
14367

14468
println!("🔈 Output device: {}", device.name()?);
14569

146-
let stream_config = audio_format.to_stream_config();
147-
let rb_clone = Arc::clone(&ring_buffer);
148-
70+
let stream_config = input.cpal_config();
14971
let output_stream = device.build_output_stream(
15072
&stream_config,
151-
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
152-
let mut rb = rb_clone.lock().unwrap();
153-
rb.read(data);
154-
},
73+
create_output_callback(buffer),
15574
|err| eprintln!("Audio output error: {}", err),
15675
None,
15776
)?;
@@ -166,7 +85,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
16685

16786
// Cleanup
16887
drop(output_stream);
169-
stream.stop_capture()?;
88+
input.stop()?;
17089

17190
println!();
17291
println!("✅ Done!");

0 commit comments

Comments
 (0)