1use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6
7use cpal::traits::{DeviceTrait, StreamTrait};
8
9use crate::Error;
10use crate::SAMPLE_RATE;
11use crate::convert::InputConverter;
12use crate::device::{Device, InputFormat, describe, input_config, open_device};
13
14pub struct Block<'a> {
16 pub samples: &'a [f32],
20 pub peak: f32,
23}
24
25pub struct Capture {
30 stop: Arc<AtomicBool>,
31 reader: Option<std::thread::JoinHandle<()>>,
32 device: Device,
35}
36
37impl Capture {
38 pub fn device(&self) -> &Device {
43 &self.device
44 }
45
46 pub fn stop(mut self) {
48 self.signal();
49 self.join();
50 }
51
52 fn signal(&self) {
53 self.stop.store(true, Ordering::Relaxed);
54 }
55
56 fn join(&mut self) {
57 if let Some(reader) = self.reader.take() {
58 let _ = reader.join();
59 }
60 }
61}
62
63impl Drop for Capture {
64 fn drop(&mut self) {
67 self.signal();
68 self.join();
69 }
70}
71
72const I16_SCALE: f32 = 32_768.0;
74
75fn i16_sample(sample: i16) -> f32 {
80 f32::from(sample) / I16_SCALE
81}
82
83std::thread_local! {
84 static PANIC_SITE: std::cell::RefCell<Option<std::backtrace::Backtrace>> =
86 const { std::cell::RefCell::new(None) };
87}
88
89fn record_panic_sites() {
96 static HOOK: std::sync::Once = std::sync::Once::new();
97 HOOK.call_once(|| {
98 let previous = std::panic::take_hook();
99 std::panic::set_hook(Box::new(move |info| {
100 PANIC_SITE.with(|slot| {
101 if let Ok(mut slot) = slot.try_borrow_mut() {
102 *slot = Some(std::backtrace::Backtrace::force_capture());
103 }
104 });
105 previous(info);
106 }));
107 });
108}
109
110fn take_panic_site() -> Option<std::backtrace::Backtrace> {
112 PANIC_SITE.with(|slot| slot.try_borrow_mut().ok().and_then(|mut slot| slot.take()))
113}
114
115fn guard_block(panicked: &AtomicBool, input: &str, body: impl FnOnce()) {
123 if panicked.load(Ordering::Acquire) {
124 return;
125 }
126 take_panic_site();
129 if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
130 panicked.store(true, Ordering::Release);
131 let why = crate::panic_message(panic.as_ref());
132 let backtrace = take_panic_site().unwrap_or_else(std::backtrace::Backtrace::force_capture);
133 tracing::warn!(
134 event_name = "input_callback_panicked",
135 input = %input,
136 why = %why,
137 backtrace = %backtrace,
138 "a panic in the audio callback was caught before it could cross the FFI boundary; \
139 this capture delivers nothing more"
140 );
141 }
142}
143
144pub fn open(
150 device: Option<&str>,
151 mut on_block: impl FnMut(Block<'_>) + Send + 'static,
152) -> Result<Capture, Error> {
153 let device = open_device(device)?;
154 let held = describe(&device);
155 let plan = input_config(&device)?;
156 let input = format!(
160 "{} ({}) {:?}{}",
161 held.name,
162 held.id,
163 plan.sample_format,
164 plan.resample_from.map(|rate| format!(", resampled from {rate} Hz")).unwrap_or_default()
165 );
166 record_panic_sites();
167 let channels = plan.config.channels as usize;
169 let mut converter = InputConverter::new(channels, plan.resample_from)?;
170 tracing::debug!(
171 event_name = "input_stream_opened",
172 channels,
173 rate = SAMPLE_RATE,
174 format = ?plan.sample_format,
175 resample_from = ?plan.resample_from,
176 "input open"
177 );
178
179 let panicked = Arc::new(AtomicBool::new(false));
182 let stop = Arc::new(AtomicBool::new(false));
183 let reader_stop = Arc::clone(&stop);
184 let (ready, started) = std::sync::mpsc::channel::<Result<(), Error>>();
185 let reader = std::thread::Builder::new()
186 .name("forge-capture".into())
187 .spawn(move || {
188 take_panic_site();
194 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
195 || -> Result<(), Error> {
196 let stream = match plan.sample_format {
200 InputFormat::F32 => {
201 let panicked = Arc::clone(&panicked);
202 let input = input.clone();
203 device.build_input_stream(
204 plan.config,
205 move |block: &[f32], _: &cpal::InputCallbackInfo| {
206 guard_block(&panicked, &input, || {
207 converter.push(block, &mut |samples, peak| {
208 on_block(Block { samples, peak });
209 });
210 });
211 },
212 |error| {
213 tracing::warn!(event_name = "input_stream_error", %error, "input stream error");
214 },
215 None,
216 )
217 }
218 InputFormat::I16 => {
219 let panicked = Arc::clone(&panicked);
220 let input = input.clone();
221 let mut scaled: Vec<f32> = Vec::new();
222 device.build_input_stream(
223 plan.config,
224 move |block: &[i16], _: &cpal::InputCallbackInfo| {
225 guard_block(&panicked, &input, || {
226 scaled.clear();
227 scaled.extend(
228 block.iter().map(|sample| i16_sample(*sample)),
229 );
230 converter.push(&scaled, &mut |samples, peak| {
231 on_block(Block { samples, peak });
232 });
233 });
234 },
235 |error| {
236 tracing::warn!(event_name = "input_stream_error", %error, "input stream error");
237 },
238 None,
239 )
240 }
241 };
242 let stream = stream
243 .map_err(|error| Error::Capture { message: error.to_string() })?;
244 stream
245 .play()
246 .map_err(|error| Error::Capture { message: error.to_string() })?;
247 let _ = ready.send(Ok(()));
248 while !reader_stop.load(Ordering::Relaxed) {
249 std::thread::sleep(Duration::from_millis(20));
250 }
251 drop(stream);
252 Ok(())
253 },
254 ));
255 match outcome {
256 Ok(Ok(())) => {}
257 Ok(Err(error)) => {
258 let _ = ready.send(Err(error));
259 }
260 Err(panic) => {
261 let why = crate::panic_message(panic.as_ref());
262 let backtrace =
263 take_panic_site().unwrap_or_else(std::backtrace::Backtrace::force_capture);
264 tracing::warn!(
265 event_name = "capture_thread_panicked",
266 input = %input,
267 why = %why,
268 backtrace = %backtrace,
269 "the capture thread panicked"
270 );
271 let _ = ready.send(Err(Error::Capture {
272 message: format!("the capture thread panicked: {why}"),
273 }));
274 }
275 }
276 })
277 .map_err(|error| Error::Capture { message: error.to_string() })?;
278
279 let capture = Capture { stop, reader: Some(reader), device: held };
280 match started.recv() {
281 Ok(Ok(())) => Ok(capture),
282 Ok(Err(error)) => {
285 capture.stop();
286 Err(error)
287 }
288 Err(_) => {
289 capture.stop();
290 Err(Error::Capture { message: "the capture thread did not start".into() })
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests_open {
297 use super::*;
298 use crate::device::devices;
299
300 #[test]
306 fn a_panicking_callback_block_is_caught_and_later_blocks_are_dropped() {
307 let panicked = AtomicBool::new(false);
308 let ran = std::cell::Cell::new(0_u32);
309 guard_block(&panicked, "a stand-in input", || {
310 ran.set(ran.get() + 1);
311 panic!("a callback panic");
312 });
313 assert!(panicked.load(Ordering::Acquire), "the first panic stops the capture");
314 guard_block(&panicked, "a stand-in input", || ran.set(ran.get() + 1));
315 assert_eq!(ran.get(), 1, "a block after the panic returns without touching the pipeline");
316 }
317
318 #[test]
321 fn an_i16_sample_is_scaled_to_full_scale_one() {
322 assert_eq!(i16_sample(0), 0.0, "silence stays silence");
323 assert_eq!(i16_sample(-32_768), -1.0, "the negative rail is -1");
324 assert!(
325 (i16_sample(32_767) - 0.999_969_5).abs() < 1e-6,
326 "the positive rail lands just under 1, as cpal's own conversion does"
327 );
328 }
329
330 #[test]
331 #[ignore = "opens the real default input; run with --run-ignored all on a machine with audio hardware"]
332 fn a_capture_reports_the_device_it_opened() {
333 let Ok(all) = devices() else { return };
337 let Some(default) = all.iter().find(|device| device.is_default) else { return };
338 let Ok(capture) = open(None, |_| {}) else { return };
339 assert_eq!(
340 capture.device().id,
341 default.id,
342 "naming no device must report the system default back to the caller"
343 );
344 assert!(capture.device().is_default, "and report it as the default");
345 capture.stop();
346 }
347}