1use pyo3::buffer::PyUntypedBuffer;
41use pyo3::prelude::*;
42use pyo3::types::{PyBytes, PyString};
43use std::collections::VecDeque;
44use std::sync::atomic::{
45 AtomicBool, AtomicI32, AtomicI64, AtomicU8, AtomicU64, AtomicUsize, Ordering,
46};
47use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
48use std::thread::{JoinHandle, ThreadId};
49use std::time::{Duration, Instant};
50
51use libpulse_binding as pulse;
52use pulse::callbacks::ListResult;
53use pulse::channelmap::Map as ChannelMap;
54use pulse::context::{Context, FlagSet as CtxFlags};
55use pulse::def::BufferAttr;
56use pulse::mainloop::standard::Mainloop;
57use pulse::sample::{Format, Spec};
58use pulse::stream::{FlagSet as StreamFlags, PeekResult, Stream};
59use pulse::time::MicroSeconds;
60
61macro_rules! plog {
64 ($($arg:tt)*) => {{
65 use std::io::Write;
66 let _ = writeln!(std::io::stdout().lock(), $($arg)*);
67 }};
68}
69macro_rules! elog {
71 ($($arg:tt)*) => {{
72 use std::io::Write;
73 let _ = writeln!(std::io::stderr().lock(), $($arg)*);
74 }};
75}
76
77mod ogg_sink;
78
79#[inline]
82fn gettid() -> i64 {
83 unsafe { libc::syscall(libc::SYS_gettid) as i64 }
84}
85
86const ST_IDLE: u8 = 0;
88const ST_STARTING: u8 = 1;
90const ST_RUNNING: u8 = 2;
92const ST_FAILED: u8 = 3;
94
95const OPUS_BITRATE_MIN: i32 = 6000;
98const OPUS_BITRATE_MAX: i32 = 510000;
99
100const STOP_NONE: i64 = 0;
102const STOP_EXTERNAL: i64 = -1;
105
106const PUMP_TIMEOUT_US: u64 = 20 * 1000;
109const MAX_OPUS_PACKET: usize = 4000;
111
112const RED_MAX_OFFSET: u64 = 16383;
114const RED_MAX_LEN: usize = 1023;
116const RED_BLOCK_PT: u8 = 0;
118const RED_MAX_DISTANCE: i32 = 4;
120const WS_QUIET: u8 = 0x80;
128
129#[cfg(test)]
162fn build_ws_body(
163 primary: &[u8],
164 primary_pts: u64,
165 history: &VecDeque<(Vec<u8>, u64)>,
166 red_distance: usize,
167 emit_header: bool,
168) -> Vec<u8> {
169 if !emit_header {
170 return primary.to_vec();
171 }
172 if red_distance == 0 {
173 let mut out = Vec::with_capacity(2 + primary.len());
174 out.push(0x01);
175 out.push(0x00);
176 out.extend_from_slice(primary);
177 return out;
178 }
179 let start = history.len().saturating_sub(red_distance);
180 let mut blocks: Vec<(&[u8], u32)> = Vec::with_capacity(red_distance);
181 for (data, pts) in history.iter().skip(start) {
182 let offset = primary_pts.saturating_sub(*pts);
183 if offset > RED_MAX_OFFSET || data.len() > RED_MAX_LEN {
184 continue;
185 }
186 blocks.push((data.as_slice(), offset as u32));
187 }
188 if blocks.is_empty() {
189 let mut out = Vec::with_capacity(2 + primary.len());
190 out.push(0x01);
191 out.push(0x00);
192 out.extend_from_slice(primary);
193 return out;
194 }
195 let redundant_bytes: usize = blocks.iter().map(|(d, _)| d.len()).sum();
196 let mut out = Vec::with_capacity(6 + 4 * blocks.len() + 1 + redundant_bytes + primary.len());
197 out.push(0x01);
198 out.push(blocks.len() as u8);
199 out.extend_from_slice(&(primary_pts as u32).to_be_bytes());
200 for (data, offset) in &blocks {
201 out.push(0x80 | (RED_BLOCK_PT & 0x7F));
202 let word = ((offset & 0x3FFF) << 10) | (data.len() as u32 & 0x3FF);
203 out.push((word >> 16) as u8);
204 out.push((word >> 8) as u8);
205 out.push(word as u8);
206 }
207 out.push(RED_BLOCK_PT & 0x7F);
208 for (data, _) in &blocks {
209 out.extend_from_slice(data);
210 }
211 out.extend_from_slice(primary);
212 out
213}
214
215const RED_PREFIX_MAX: usize =
217 6 + 4 * RED_MAX_DISTANCE as usize + 1 + RED_MAX_DISTANCE as usize * RED_MAX_LEN;
218
219fn write_ws_prefix_into(
235 buf: &mut [u8],
236 primary_pts: u64,
237 history: &VecDeque<(Vec<u8>, u64)>,
238 red_distance: usize,
239 emit_header: bool,
240) -> usize {
241 if !emit_header {
242 return 0;
243 }
244 if red_distance > 0 {
245 debug_assert!(red_distance <= RED_MAX_DISTANCE as usize);
246 let start = history.len().saturating_sub(red_distance);
247 let mut idx = [0usize; RED_MAX_DISTANCE as usize];
248 let mut n = 0usize;
249 for (i, (data, pts)) in history.iter().enumerate().skip(start) {
250 let offset = primary_pts.saturating_sub(*pts);
251 if offset <= RED_MAX_OFFSET && data.len() <= RED_MAX_LEN {
252 idx[n] = i;
253 n += 1;
254 }
255 }
256 if n > 0 {
257 buf[0] = 0x01;
258 buf[1] = n as u8;
259 buf[2..6].copy_from_slice(&(primary_pts as u32).to_be_bytes());
260 let mut i = 6;
261 for &k in &idx[..n] {
262 let (data, pts) = &history[k];
263 let offset = primary_pts.saturating_sub(*pts) as u32;
264 buf[i] = 0x80 | (RED_BLOCK_PT & 0x7F);
265 let word = ((offset & 0x3FFF) << 10) | (data.len() as u32 & 0x3FF);
266 buf[i + 1] = (word >> 16) as u8;
267 buf[i + 2] = (word >> 8) as u8;
268 buf[i + 3] = word as u8;
269 i += 4;
270 }
271 buf[i] = RED_BLOCK_PT & 0x7F;
272 i += 1;
273 for &k in &idx[..n] {
274 let data = &history[k].0;
275 buf[i..i + data.len()].copy_from_slice(data);
276 i += data.len();
277 }
278 return i;
279 }
280 }
281 buf[0] = 0x01;
282 buf[1] = 0x00;
283 2
284}
285
286#[derive(Clone)]
288struct Settings {
289 device_name: Option<String>,
290 sample_rate: u32,
291 channels: i32,
292 opus_bitrate: i32,
293 frame_duration_ms: f64,
294 use_vbr: bool,
295 opus_complexity: Option<i32>,
296 use_silence_gate: bool,
297 debug_logging: bool,
298 latency_ms: i32,
299 omit_audio_header: bool,
300 red_distance: i32,
301 output_socket: String,
302}
303
304const OPUS_COMPLEXITY_MAX: i32 = 10;
306
307fn valid_opus_duration(ms: f64) -> bool {
312 matches!((ms * 10.0).round() as i64, 25 | 50 | 100 | 200 | 400 | 600)
313}
314
315fn parse_device_name(dev_obj: &Bound<'_, PyAny>) -> PyResult<Option<String>> {
322 if dev_obj.is_none() {
323 return Ok(None);
324 }
325 let v: String = if let Ok(st) = dev_obj.cast::<PyString>() {
326 st.to_str()?.to_string()
327 } else if let Ok(b) = dev_obj.cast::<PyBytes>() {
328 String::from_utf8_lossy(b.as_bytes()).into_owned()
329 } else {
330 dev_obj.extract()?
331 };
332 if v.contains('\0') {
333 return Err(pyo3::exceptions::PyValueError::new_err(
334 "device_name must not contain NUL bytes",
335 ));
336 }
337 Ok(if v.is_empty() { None } else { Some(v) })
338}
339
340fn extract_settings(s: &Bound<'_, PyAny>) -> PyResult<Settings> {
348 let device_name = parse_device_name(&s.getattr("device_name")?)?;
349 let parsed = Settings {
350 device_name,
351 sample_rate: s.getattr("sample_rate")?.extract()?,
352 channels: s.getattr("channels")?.extract()?,
353 opus_bitrate: s
354 .getattr("opus_bitrate")?
355 .extract::<i32>()?
356 .clamp(OPUS_BITRATE_MIN, OPUS_BITRATE_MAX),
357 frame_duration_ms: s.getattr("frame_duration_ms")?.extract()?,
358 use_vbr: s.getattr("use_vbr")?.extract()?,
359 opus_complexity: s.getattr("opus_complexity")?.extract()?,
360 use_silence_gate: s.getattr("use_silence_gate")?.extract()?,
361 debug_logging: s.getattr("debug_logging")?.extract()?,
362 latency_ms: s.getattr("latency_ms")?.extract()?,
363 omit_audio_header: s.getattr("omit_audio_header")?.extract()?,
364 red_distance: s
365 .getattr("red_distance")?
366 .extract::<i32>()?
367 .clamp(0, RED_MAX_DISTANCE),
368 output_socket: s.getattr("output_socket")?.extract()?,
369 };
370 check_opus_sample_rate(parsed.sample_rate)?;
371 if !valid_opus_duration(parsed.frame_duration_ms) {
372 return value_error(format!(
373 "frame_duration_ms must be one of 2.5, 5, 10, 20, 40, or 60 (got {})",
374 parsed.frame_duration_ms
375 ));
376 }
377 if let Some(c) = parsed.opus_complexity
378 && !(0..=OPUS_COMPLEXITY_MAX).contains(&c)
379 {
380 return value_error(format!(
381 "opus_complexity must be None or between 0 and {OPUS_COMPLEXITY_MAX} (got {c})"
382 ));
383 }
384 if !matches!(parsed.channels, 1 | 2 | 6 | 8) {
385 return value_error(format!(
386 "channels must be 1, 2, 6, or 8 (got {})",
387 parsed.channels
388 ));
389 }
390 if parsed.latency_ms < 0 {
391 return value_error(format!(
392 "latency_ms must be >= 0 (got {}); 0 selects the default fragment size",
393 parsed.latency_ms
394 ));
395 }
396 Ok(parsed)
397}
398
399fn value_error<T>(msg: String) -> PyResult<T> {
401 Err(pyo3::exceptions::PyValueError::new_err(msg))
402}
403
404fn check_opus_sample_rate(rate: u32) -> PyResult<()> {
407 if matches!(rate, 8000 | 12000 | 16000 | 24000 | 48000) {
408 Ok(())
409 } else {
410 value_error(format!(
411 "sample_rate must be 8000, 12000, 16000, 24000, or 48000 (got {rate})"
412 ))
413 }
414}
415
416#[derive(Clone)]
419struct PbSettings {
420 device_name: Option<String>,
421 sample_rate: u32,
422 channels: i32,
423 latency_ms: i32,
424 max_buffer_bytes: usize,
425 debug_logging: bool,
426}
427
428fn extract_pb_settings(s: &Bound<'_, PyAny>) -> PyResult<PbSettings> {
436 let parsed = PbSettings {
437 device_name: parse_device_name(&s.getattr("device_name")?)?,
438 sample_rate: s.getattr("sample_rate")?.extract()?,
439 channels: s.getattr("channels")?.extract()?,
440 latency_ms: s.getattr("latency_ms")?.extract()?,
441 max_buffer_bytes: s.getattr("max_buffer_bytes")?.extract()?,
442 debug_logging: s.getattr("debug_logging")?.extract()?,
443 };
444 check_opus_sample_rate(parsed.sample_rate)?;
445 if !matches!(parsed.channels, 1 | 2) {
446 return value_error(format!(
447 "playback channels must be 1 or 2 (got {})",
448 parsed.channels
449 ));
450 }
451 if parsed.latency_ms <= 0 {
452 return value_error(format!(
453 "latency_ms must be > 0 (got {})",
454 parsed.latency_ms
455 ));
456 }
457 if parsed.max_buffer_bytes == 0 {
458 return value_error("max_buffer_bytes must be > 0 (got 0)".to_string());
459 }
460 Ok(parsed)
461}
462
463#[pyclass(dict)]
469struct AudioCaptureSettings {
470 #[pyo3(get, set)]
471 device_name: Py<PyAny>,
472 #[pyo3(get, set)]
473 sample_rate: u32,
474 #[pyo3(get, set)]
475 channels: i32,
476 #[pyo3(get, set)]
477 opus_bitrate: i32,
478 #[pyo3(get, set)]
479 frame_duration_ms: f64,
480 #[pyo3(get, set)]
481 use_vbr: bool,
482 #[pyo3(get, set)]
486 opus_complexity: Option<i32>,
487 #[pyo3(get, set)]
488 use_silence_gate: bool,
489 #[pyo3(get, set)]
490 debug_logging: bool,
491 #[pyo3(get, set)]
492 latency_ms: i32,
493 #[pyo3(get, set)]
494 omit_audio_header: bool,
495 #[pyo3(get, set)]
496 red_distance: i32,
497 #[pyo3(get, set)]
501 output_socket: String,
502}
503
504#[pymethods]
505impl AudioCaptureSettings {
506 #[new]
507 fn new(py: Python<'_>) -> Self {
508 AudioCaptureSettings {
509 device_name: py.None(),
510 sample_rate: 48000,
511 channels: 2,
512 opus_bitrate: 128000,
513 frame_duration_ms: 20.0,
514 use_vbr: true,
515 opus_complexity: None,
516 use_silence_gate: true,
517 debug_logging: false,
518 latency_ms: 0,
519 omit_audio_header: false,
520 red_distance: 0,
521 output_socket: String::new(),
522 }
523 }
524}
525
526#[pyclass]
532struct AudioPlaybackSettings {
533 #[pyo3(get, set)]
534 device_name: Py<PyAny>,
535 #[pyo3(get, set)]
536 sample_rate: u32,
537 #[pyo3(get, set)]
538 channels: i32,
539 #[pyo3(get, set)]
540 latency_ms: i32,
541 #[pyo3(get, set)]
542 max_buffer_bytes: usize,
543 #[pyo3(get, set)]
544 debug_logging: bool,
545}
546
547#[pymethods]
548impl AudioPlaybackSettings {
549 #[new]
550 fn new(py: Python<'_>) -> Self {
551 AudioPlaybackSettings {
552 device_name: PyString::new(py, "input").into_any().unbind(),
553 sample_rate: 24000,
554 channels: 1,
555 latency_ms: 40,
556 max_buffer_bytes: 96000,
557 debug_logging: false,
558 }
559 }
560}
561
562#[pyclass]
569struct AudioFrame {
570 data: Vec<u8>,
571 pts: u64,
572 channels: u8,
574 pool: Option<Arc<BufferPool>>,
576}
577
578impl Drop for AudioFrame {
579 fn drop(&mut self) {
580 if let Some(pool) = self.pool.take() {
581 pool.put(std::mem::take(&mut self.data));
582 }
583 }
584}
585
586#[pymethods]
587impl AudioFrame {
588 fn __len__(&self) -> usize {
589 self.data.len()
590 }
591
592 #[getter]
593 fn pts(&self) -> u64 {
594 self.pts
595 }
596
597 #[getter]
600 fn channels(&self) -> u8 {
601 self.channels
602 }
603
604 unsafe fn __getbuffer__(
610 slf: PyRefMut<'_, Self>,
611 view: *mut pyo3::ffi::Py_buffer,
612 flags: std::os::raw::c_int,
613 ) -> PyResult<()> {
614 let r = unsafe {
615 pyo3::ffi::PyBuffer_FillInfo(
616 view,
617 slf.as_ptr(),
618 slf.data.as_ptr() as *mut std::os::raw::c_void,
619 slf.data.len() as pyo3::ffi::Py_ssize_t,
620 1,
621 flags,
622 )
623 };
624 if r != 0 {
625 return Err(PyErr::fetch(slf.py()));
626 }
627 Ok(())
628 }
629
630 unsafe fn __releasebuffer__(&self, _view: *mut pyo3::ffi::Py_buffer) {}
631}
632
633struct Inner {
646 stop_state: AtomicI64,
652 started_ok: AtomicBool,
653 start_state: AtomicU8,
654 last_error: Mutex<Option<String>>,
659 capture_tid: AtomicI64,
661 deliver_tid: AtomicI64,
666 opus_bitrate: AtomicI32,
670 use_silence_gate: AtomicBool,
671 debug_logging: AtomicBool,
672 emit_audio_header: AtomicBool,
673 stereo_companion: AtomicBool,
675}
676
677impl Inner {
678 fn new() -> Self {
679 Inner {
680 stop_state: AtomicI64::new(STOP_NONE),
681 started_ok: AtomicBool::new(false),
682 start_state: AtomicU8::new(ST_IDLE),
683 last_error: Mutex::new(None),
684 capture_tid: AtomicI64::new(0),
685 deliver_tid: AtomicI64::new(0),
686 opus_bitrate: AtomicI32::new(128000),
687 use_silence_gate: AtomicBool::new(true),
688 debug_logging: AtomicBool::new(false),
689 emit_audio_header: AtomicBool::new(true),
690 stereo_companion: AtomicBool::new(false),
691 }
692 }
693
694 fn request_external_stop(&self) {
701 self.stop_state.store(STOP_EXTERNAL, Ordering::Release);
702 }
703
704 fn request_self_stop(&self, me: i64) {
709 let _ =
710 self.stop_state
711 .compare_exchange(STOP_NONE, me, Ordering::AcqRel, Ordering::Acquire);
712 }
713
714 fn undo_self_stop(&self, me: i64) {
720 let _ =
721 self.stop_state
722 .compare_exchange(me, STOP_NONE, Ordering::AcqRel, Ordering::Acquire);
723 }
724
725 fn clear_stop(&self) {
729 self.stop_state.store(STOP_NONE, Ordering::Release);
730 }
731
732 fn stop_pending(&self) -> bool {
734 self.stop_state.load(Ordering::Acquire) != STOP_NONE
735 }
736
737 fn worker_alive(&self) -> bool {
745 self.start_state.load(Ordering::Acquire) == ST_STARTING || self.running()
746 }
747
748 fn running(&self) -> bool {
751 self.started_ok.load(Ordering::Acquire) && !self.stop_pending()
752 }
753
754 fn state_name(&self) -> &'static str {
760 match self.start_state.load(Ordering::Acquire) {
761 ST_STARTING => "starting",
762 ST_FAILED => "failed",
763 ST_RUNNING if self.running() => "running",
764 _ => "idle",
765 }
766 }
767
768 fn fail(&self, msg: String) {
772 elog!("[pcmflux] ERROR: {msg}");
773 *self.last_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(msg);
774 self.started_ok.store(false, Ordering::Release);
775 self.start_state.store(ST_FAILED, Ordering::Release);
776 }
777
778 fn last_error(&self) -> Option<String> {
781 self.last_error
782 .lock()
783 .unwrap_or_else(|e| e.into_inner())
784 .clone()
785 }
786
787 fn clear_error(&self) {
789 *self.last_error.lock().unwrap_or_else(|e| e.into_inner()) = None;
790 }
791
792 fn is_own_thread(&self, me: i64) -> bool {
798 self.capture_tid.load(Ordering::Acquire) == me
799 || self.deliver_tid.load(Ordering::Acquire) == me
800 }
801}
802
803struct Shared {
806 inner: Arc<Inner>,
807 thread: Mutex<Option<JoinHandle<()>>>,
809}
810
811static REGISTRY: OnceLock<Mutex<Vec<Weak<Shared>>>> = OnceLock::new();
814fn registry() -> &'static Mutex<Vec<Weak<Shared>>> {
816 REGISTRY.get_or_init(|| Mutex::new(Vec::new()))
817}
818
819fn join_held_worker(slot: &mut Option<JoinHandle<()>>, inner: &Inner) {
823 if let Some(handle) = slot.take() {
824 inner.request_external_stop();
825 let _ = handle.join();
826 inner.capture_tid.store(0, Ordering::Release);
827 }
828}
829
830fn stop_and_join(slot: &Mutex<Option<JoinHandle<()>>>, inner: &Inner) {
834 join_held_worker(&mut slot.lock().unwrap_or_else(|e| e.into_inner()), inner);
835}
836
837fn spawn_worker(
855 slot: &Mutex<Option<JoinHandle<()>>>,
856 inner: &Arc<Inner>,
857 name: &str,
858 body: impl FnOnce() + Send + 'static,
859) -> Option<ThreadId> {
860 let mut guard = slot.lock().unwrap_or_else(|e| e.into_inner());
861 join_held_worker(&mut guard, inner);
862 inner.clear_stop();
863 inner.clear_error();
864 inner.started_ok.store(false, Ordering::Release);
865 inner.start_state.store(ST_STARTING, Ordering::Release);
866 let t_inner = inner.clone();
867 match std::thread::Builder::new()
868 .name(name.into())
869 .spawn(move || {
870 unsafe {
871 let _ = libc::setpriority(libc::PRIO_PROCESS, gettid() as libc::id_t, -15);
872 }
873 t_inner.capture_tid.store(gettid(), Ordering::Release);
874 if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
878 let what = payload
879 .downcast_ref::<&str>()
880 .map(|s| s.to_string())
881 .or_else(|| payload.downcast_ref::<String>().cloned())
882 .unwrap_or_else(|| "unknown panic payload".to_string());
883 t_inner.fail(format!("worker thread panicked: {what}"));
884 }
885 t_inner.capture_tid.store(0, Ordering::Release);
886 }) {
887 Ok(h) => {
888 let id = h.thread().id();
889 *guard = Some(h);
890 Some(id)
891 }
892 Err(_) => None,
893 }
894}
895
896fn join_failed_start(slot: &Mutex<Option<JoinHandle<()>>>, inner: &Inner, spawned: ThreadId) {
904 let mut guard = slot.lock().unwrap_or_else(|e| e.into_inner());
905 if guard.as_ref().map(|h| h.thread().id()) != Some(spawned) {
906 return;
907 }
908 join_held_worker(&mut guard, inner);
909}
910
911fn await_start(
919 py: Python<'_>,
920 slot: &Mutex<Option<JoinHandle<()>>>,
921 inner: &Inner,
922 spawned: ThreadId,
923 what: &str,
924) -> PyResult<()> {
925 let mut state = inner.start_state.load(Ordering::Acquire);
926 if state == ST_STARTING {
927 py.detach(|| {
928 for _ in 0..200 {
929 state = inner.start_state.load(Ordering::Acquire);
930 if state != ST_STARTING {
931 break;
932 }
933 std::thread::sleep(Duration::from_millis(10));
934 }
935 });
936 }
937 if state == ST_FAILED {
938 py.detach(|| join_failed_start(slot, inner, spawned));
939 let why = inner
940 .last_error()
941 .unwrap_or_else(|| "unknown error".to_string());
942 return Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
943 "{what} failed to start: {why}"
944 )));
945 }
946 Ok(())
947}
948
949struct PlayQueue {
957 buf: Mutex<VecDeque<u8>>,
958 max_bytes: AtomicUsize,
961 frame_bytes: AtomicUsize,
962}
963
964impl PlayQueue {
965 fn new() -> Self {
966 PlayQueue {
967 buf: Mutex::new(VecDeque::new()),
968 max_bytes: AtomicUsize::new(96000),
969 frame_bytes: AtomicUsize::new(2),
970 }
971 }
972
973 fn configure(&self, max_bytes: usize, frame_bytes: usize) {
978 let fb = frame_bytes.max(1);
979 self.frame_bytes.store(fb, Ordering::Relaxed);
980 self.max_bytes
981 .store((max_bytes / fb * fb).max(fb), Ordering::Relaxed);
982 self.clear();
983 }
984
985 fn clear(&self) {
988 self.buf.lock().unwrap_or_else(|e| e.into_inner()).clear();
989 }
990
991 fn push(&self, data: &[u8]) {
996 let max = self.max_bytes.load(Ordering::Relaxed);
997 let fb = self.frame_bytes.load(Ordering::Relaxed);
998 let mut q = self.buf.lock().unwrap_or_else(|e| e.into_inner());
999 q.extend(data.iter().copied());
1000 let over = q.len().saturating_sub(max);
1001 if over > 0 {
1002 let drop = (over.div_ceil(fb) * fb).min(q.len());
1003 q.drain(..drop);
1004 }
1005 }
1006
1007 fn drain_upto(&self, n: usize, out: &mut Vec<u8>) -> usize {
1011 let fb = self.frame_bytes.load(Ordering::Relaxed);
1012 let mut q = self.buf.lock().unwrap_or_else(|e| e.into_inner());
1013 let mut take = n.min(q.len());
1014 take -= take % fb;
1015 out.clear();
1016 out.extend(q.drain(..take));
1017 q.len()
1018 }
1019
1020 fn splice_head(&self, cut: usize, fade: usize) {
1026 let fb = self.frame_bytes.load(Ordering::Relaxed);
1027 let (cut, fade) = (cut / fb * fb, fade / fb * fb);
1028 let mut q = self.buf.lock().unwrap_or_else(|e| e.into_inner());
1029 if cut == 0 || cut + fade > q.len() {
1030 return;
1031 }
1032 let channels = (fb / 2).max(1);
1033 let frames = (fade / fb) as f32;
1034 for i in (0..fade).step_by(2) {
1035 let w = ((i / 2 / channels) as f32 + 1.0) / (frames + 1.0);
1036 let a = i16::from_le_bytes([q[i], q[i + 1]]) as f32;
1037 let b = i16::from_le_bytes([q[cut + i], q[cut + i + 1]]) as f32;
1038 let [lo, hi] = ((a + (b - a) * w).round() as i16).to_le_bytes();
1039 q[cut + i] = lo;
1040 q[cut + i + 1] = hi;
1041 }
1042 q.drain(..cut);
1043 }
1044}
1045
1046struct StandingDepth {
1058 window: usize,
1059 keep: usize,
1060 min_cut: usize,
1061 frame_bytes: usize,
1062 written: usize,
1063 min_left: usize,
1064}
1065
1066impl StandingDepth {
1067 fn new(bytes_per_sec: usize, frame_bytes: usize) -> Self {
1070 let fb = frame_bytes.max(1);
1071 let frames = |ms: usize| (bytes_per_sec * ms / 1000 / fb).max(1) * fb;
1072 StandingDepth {
1073 window: frames(1000),
1074 keep: frames(2),
1075 min_cut: frames(10),
1076 frame_bytes: fb,
1077 written: 0,
1078 min_left: usize::MAX,
1079 }
1080 }
1081
1082 fn reset(&mut self) {
1084 self.written = 0;
1085 self.min_left = usize::MAX;
1086 }
1087
1088 fn after_drain(&mut self, written: usize, left: usize) -> Option<usize> {
1091 self.written += written;
1092 self.min_left = self.min_left.min(left);
1093 if self.written < self.window {
1094 return None;
1095 }
1096 let excess = self.min_left.saturating_sub(self.keep);
1097 self.reset();
1098 (excess >= self.min_cut).then(|| excess / self.frame_bytes * self.frame_bytes)
1099 }
1100}
1101
1102struct OpusPlaybackDecoder {
1108 dec: OpusDec,
1109 channels: usize,
1110 pcm: Vec<i16>,
1111 packet: Vec<u8>,
1114 last_ts: Option<i64>,
1118}
1119
1120struct OpusDec {
1125 st: *mut opusic_sys::OpusDecoder,
1126}
1127
1128unsafe impl Send for OpusDec {}
1129
1130impl Drop for OpusDec {
1131 fn drop(&mut self) {
1132 unsafe { opusic_sys::opus_decoder_destroy(self.st) }
1133 }
1134}
1135
1136impl OpusPlaybackDecoder {
1137 fn new(sample_rate: u32, channels: i32) -> Option<Self> {
1140 let mut err = 0;
1141 let st = unsafe {
1142 opusic_sys::opus_decoder_create(
1143 sample_rate as i32,
1144 if channels <= 1 { 1 } else { 2 },
1145 &mut err,
1146 )
1147 };
1148 if st.is_null() || err != 0 {
1149 return None;
1150 }
1151 let dec = OpusDec { st };
1152 Some(OpusPlaybackDecoder {
1153 dec,
1154 channels: channels.max(1) as usize,
1155 pcm: Vec::new(),
1156 packet: Vec::new(),
1157 last_ts: None,
1158 })
1159 }
1160
1161 fn decode_to_pcm(&mut self, packet: &[u8]) -> Option<&[u8]> {
1171 if packet.is_empty() {
1172 return None;
1173 }
1174 let cap = 5760 * self.channels;
1175 if self.pcm.len() < cap {
1176 self.pcm.resize(cap, 0);
1177 }
1178 let samples = unsafe {
1179 opusic_sys::opus_decode(
1180 self.dec.st,
1181 packet.as_ptr(),
1182 packet.len() as i32,
1183 self.pcm.as_mut_ptr(),
1184 (cap / self.channels) as i32,
1185 0,
1186 )
1187 };
1188 if samples < 0 {
1189 return None;
1190 }
1191 let n = samples as usize * self.channels;
1192 Some(bytemuck::cast_slice(&self.pcm[..n]))
1193 }
1194
1195 fn decode_red_into_queue(&mut self, payload: &[u8], primary_ts: i64, queue: &PlayQueue) {
1218 let n = payload.len();
1219 let mut i = 0usize;
1220 let mut offs = [0i64; RED_MAX_DISTANCE as usize];
1221 let mut lens = [0usize; RED_MAX_DISTANCE as usize];
1222 let mut nh = 0usize;
1223 while i < n && (payload[i] & 0x80) != 0 {
1224 if i + 4 > n || nh >= RED_MAX_DISTANCE as usize {
1225 return;
1226 }
1227 let field = ((payload[i + 1] as u32) << 16)
1228 | ((payload[i + 2] as u32) << 8)
1229 | (payload[i + 3] as u32);
1230 offs[nh] = ((field >> 10) & 0x3FFF) as i64;
1231 lens[nh] = (field & 0x3FF) as usize;
1232 nh += 1;
1233 i += 4;
1234 }
1235 if i >= n {
1236 return;
1237 }
1238 i += 1;
1239
1240 let mut frames = [(0i64, 0usize, 0usize); RED_MAX_DISTANCE as usize + 1];
1241 let mut nf = 0usize;
1242 for k in 0..nh {
1243 if i + lens[k] > n {
1244 return;
1245 }
1246 if lens[k] > 0 {
1247 frames[nf] = (primary_ts - offs[k], i, lens[k]);
1248 nf += 1;
1249 }
1250 i += lens[k];
1251 }
1252 frames[nf] = (primary_ts, i, n - i);
1253 nf += 1;
1254
1255 if self.last_ts.is_none() {
1256 let (ts, start, len) = frames[nf - 1];
1257 if len > 0
1258 && let Some(pcm) = self.decode_to_pcm(&payload[start..start + len])
1259 {
1260 queue.push(pcm);
1261 }
1262 self.last_ts = Some(ts);
1263 return;
1264 }
1265 let mut last = self.last_ts.unwrap();
1266 for &(ts, start, len) in frames.iter().take(nf) {
1267 let newer = ((ts.wrapping_sub(last)) as i32) > 0;
1271 if len > 0 && newer {
1272 if let Some(pcm) = self.decode_to_pcm(&payload[start..start + len]) {
1273 queue.push(pcm);
1274 }
1275 last = ts;
1276 }
1277 }
1278 self.last_ts = Some(last);
1279 }
1280}
1281
1282struct PbShared {
1288 inner: Arc<Inner>,
1289 thread: Mutex<Option<JoinHandle<()>>>,
1290 queue: Arc<PlayQueue>,
1291 opus_dec: Mutex<Option<OpusPlaybackDecoder>>,
1294}
1295
1296static PLAYBACK_REGISTRY: OnceLock<Mutex<Vec<Weak<PbShared>>>> = OnceLock::new();
1299fn playback_registry() -> &'static Mutex<Vec<Weak<PbShared>>> {
1301 PLAYBACK_REGISTRY.get_or_init(|| Mutex::new(Vec::new()))
1302}
1303
1304fn pump(ml: &mut pulse::mainloop::standard::Mainloop, timeout_us: u64) -> bool {
1311 if ml.prepare(Some(MicroSeconds(timeout_us))).is_err() {
1312 return false;
1313 }
1314 if ml.poll().is_err() {
1315 return false;
1316 }
1317 if ml.dispatch().is_err() {
1318 return false;
1319 }
1320 true
1321}
1322
1323fn multiopus_layout(channels: i32) -> Option<(i32, i32, &'static [u8])> {
1329 match channels {
1330 6 => Some((4, 2, &[0, 4, 1, 2, 3, 5])),
1331 8 => Some((5, 3, &[0, 6, 1, 2, 3, 4, 5, 7])),
1332 _ => None,
1333 }
1334}
1335
1336fn surround_channel_map(channels: i32) -> Option<&'static str> {
1345 match channels {
1346 6 => Some("front-left,front-right,front-center,lfe,rear-left,rear-right"),
1347 8 => Some(
1348 "front-left,front-right,front-center,lfe,rear-left,rear-right,side-left,side-right",
1349 ),
1350 _ => None,
1351 }
1352}
1353
1354fn downmix_stereo(src: &[i16], channels: usize, dst: &mut [i16]) {
1360 const K: f32 = std::f32::consts::FRAC_1_SQRT_2;
1361 for (f, out) in dst.as_chunks_mut::<2>().0.iter_mut().enumerate() {
1362 let s = &src[f * channels..(f + 1) * channels];
1363 let center = K * s[2] as f32;
1364 let mut left = s[0] as f32 + center + K * s[4] as f32;
1365 let mut right = s[1] as f32 + center + K * s[5] as f32;
1366 if channels >= 8 {
1367 left += K * s[6] as f32;
1368 right += K * s[7] as f32;
1369 }
1370 out[0] = left.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
1371 out[1] = right.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
1372 }
1373}
1374
1375enum PcmEncoder {
1378 Stereo(MonoOpus),
1379 Multi(MultiOpus),
1380}
1381
1382struct MonoOpus {
1386 st: *mut opusic_sys::OpusEncoder,
1387}
1388
1389unsafe impl Send for MonoOpus {}
1390
1391impl Drop for MonoOpus {
1392 fn drop(&mut self) {
1393 unsafe { opusic_sys::opus_encoder_destroy(self.st) }
1394 }
1395}
1396
1397struct MultiOpus {
1403 st: *mut opusic_sys::OpusMSEncoder,
1404}
1405
1406unsafe impl Send for MultiOpus {}
1407
1408impl Drop for MultiOpus {
1409 fn drop(&mut self) {
1410 unsafe { opusic_sys::opus_multistream_encoder_destroy(self.st) }
1411 }
1412}
1413
1414impl PcmEncoder {
1415 fn new(
1426 sample_rate: u32,
1427 channels: i32,
1428 vbr: bool,
1429 bitrate: i32,
1430 complexity: Option<i32>,
1431 ) -> Result<Self, String> {
1432 if channels <= 2 {
1433 let mut err = 0;
1434 let st = unsafe {
1435 opusic_sys::opus_encoder_create(
1436 sample_rate as i32,
1437 if channels == 1 { 1 } else { 2 },
1438 opusic_sys::OPUS_APPLICATION_RESTRICTED_LOWDELAY,
1439 &mut err,
1440 )
1441 };
1442 if st.is_null() || err != 0 {
1443 return Err(format!("opus_encoder_create() failed: {err}"));
1444 }
1445 let enc = MonoOpus { st };
1446 unsafe {
1447 if opusic_sys::opus_encoder_ctl(st, opusic_sys::OPUS_SET_BITRATE_REQUEST, bitrate)
1448 != 0
1449 {
1450 elog!("[pcmflux] WARNING: failed to apply initial bitrate");
1451 }
1452 if opusic_sys::opus_encoder_ctl(
1453 st,
1454 opusic_sys::OPUS_SET_VBR_REQUEST,
1455 i32::from(vbr),
1456 ) != 0
1457 {
1458 elog!("[pcmflux] WARNING: failed to apply VBR mode");
1459 }
1460 if let Some(c) = complexity
1461 && opusic_sys::opus_encoder_ctl(st, opusic_sys::OPUS_SET_COMPLEXITY_REQUEST, c)
1462 != 0
1463 {
1464 elog!("[pcmflux] WARNING: failed to apply complexity");
1465 }
1466 }
1467 return Ok(PcmEncoder::Stereo(enc));
1468 }
1469 let (streams, coupled, mapping) = multiopus_layout(channels)
1470 .ok_or_else(|| format!("unsupported surround channel count {channels}"))?;
1471 unsafe {
1472 let mut err: i32 = 0;
1473 let st = opusic_sys::opus_multistream_encoder_create(
1474 sample_rate as i32,
1475 channels,
1476 streams,
1477 coupled,
1478 mapping.as_ptr(),
1479 opusic_sys::OPUS_APPLICATION_RESTRICTED_LOWDELAY,
1480 &mut err,
1481 );
1482 if st.is_null() || err != 0 {
1483 return Err(format!("opus_multistream_encoder_create() failed: {err}"));
1484 }
1485 if opusic_sys::opus_multistream_encoder_ctl(
1486 st,
1487 opusic_sys::OPUS_SET_BITRATE_REQUEST,
1488 bitrate,
1489 ) != 0
1490 {
1491 elog!("[pcmflux] WARNING: failed to apply initial surround bitrate");
1492 }
1493 if opusic_sys::opus_multistream_encoder_ctl(
1494 st,
1495 opusic_sys::OPUS_SET_VBR_REQUEST,
1496 vbr as i32,
1497 ) != 0
1498 {
1499 elog!("[pcmflux] WARNING: failed to apply surround VBR mode");
1500 }
1501 if let Some(c) = complexity
1502 && opusic_sys::opus_multistream_encoder_ctl(
1503 st,
1504 opusic_sys::OPUS_SET_COMPLEXITY_REQUEST,
1505 c,
1506 ) != 0
1507 {
1508 elog!("[pcmflux] WARNING: failed to apply surround complexity");
1509 }
1510 Ok(PcmEncoder::Multi(MultiOpus { st }))
1511 }
1512 }
1513
1514 fn encode(
1519 &mut self,
1520 pcm: &[i16],
1521 frame_size_per_channel: usize,
1522 out: &mut [u8],
1523 ) -> Result<usize, String> {
1524 match self {
1525 PcmEncoder::Stereo(enc) => unsafe {
1526 let n = opusic_sys::opus_encode(
1527 enc.st,
1528 pcm.as_ptr(),
1529 frame_size_per_channel as i32,
1530 out.as_mut_ptr(),
1531 out.len() as i32,
1532 );
1533 if n < 0 {
1534 Err(format!("opus_encode() failed: {n}"))
1535 } else {
1536 Ok(n as usize)
1537 }
1538 },
1539 PcmEncoder::Multi(ms) => unsafe {
1540 let n = opusic_sys::opus_multistream_encode(
1541 ms.st,
1542 pcm.as_ptr(),
1543 frame_size_per_channel as i32,
1544 out.as_mut_ptr(),
1545 out.len() as i32,
1546 );
1547 if n < 0 {
1548 Err(format!("opus_multistream_encode() failed: {n}"))
1549 } else {
1550 Ok(n as usize)
1551 }
1552 },
1553 }
1554 }
1555
1556 fn lookahead(&mut self) -> i32 {
1558 match self {
1559 PcmEncoder::Stereo(enc) => unsafe {
1560 let mut value: i32 = 0;
1561 let ret = opusic_sys::opus_encoder_ctl(
1562 enc.st,
1563 opusic_sys::OPUS_GET_LOOKAHEAD_REQUEST,
1564 &mut value as *mut i32,
1565 );
1566 if ret == 0 { value } else { 0 }
1567 },
1568 PcmEncoder::Multi(ms) => unsafe {
1569 let mut value: i32 = 0;
1570 let ret = opusic_sys::opus_multistream_encoder_ctl(
1571 ms.st,
1572 opusic_sys::OPUS_GET_LOOKAHEAD_REQUEST,
1573 &mut value as *mut i32,
1574 );
1575 if ret == 0 { value } else { 0 }
1576 },
1577 }
1578 }
1579
1580 fn set_bitrate(&mut self, bits: i32) -> Result<(), String> {
1582 match self {
1583 PcmEncoder::Stereo(enc) => unsafe {
1584 let ret = opusic_sys::opus_encoder_ctl(
1585 enc.st,
1586 opusic_sys::OPUS_SET_BITRATE_REQUEST,
1587 bits,
1588 );
1589 if ret != 0 {
1590 Err(format!("ctl error {ret}"))
1591 } else {
1592 Ok(())
1593 }
1594 },
1595 PcmEncoder::Multi(ms) => unsafe {
1596 let ret = opusic_sys::opus_multistream_encoder_ctl(
1597 ms.st,
1598 opusic_sys::OPUS_SET_BITRATE_REQUEST,
1599 bits,
1600 );
1601 if ret != 0 {
1602 Err(format!("ctl error {ret}"))
1603 } else {
1604 Ok(())
1605 }
1606 },
1607 }
1608 }
1609}
1610
1611type FrameQueue = Option<VecDeque<(Vec<u8>, u64, u8)>>;
1614
1615struct DeliveryRing {
1623 q: Mutex<FrameQueue>,
1624 cv: Condvar,
1625 dropped: AtomicU64,
1626 capacity: usize,
1627}
1628
1629impl DeliveryRing {
1630 fn new(capacity: usize) -> Self {
1632 Self {
1633 q: Mutex::new(Some(VecDeque::with_capacity(capacity))),
1634 cv: Condvar::new(),
1635 dropped: AtomicU64::new(0),
1636 capacity,
1637 }
1638 }
1639
1640 fn push(&self, data: Vec<u8>, pts: u64, channels: u8) -> Option<Vec<u8>> {
1643 let mut g = self.q.lock().unwrap_or_else(|e| e.into_inner());
1644 let Some(q) = g.as_mut() else {
1645 return Some(data);
1646 };
1647 if q.len() >= self.capacity {
1648 q.pop_front();
1649 self.dropped.fetch_add(1, Ordering::Relaxed);
1650 }
1651 q.push_back((data, pts, channels));
1652 self.cv.notify_one();
1653 None
1654 }
1655
1656 fn pop(&self) -> Option<(Vec<u8>, u64, u8)> {
1659 let mut g = self.q.lock().unwrap_or_else(|e| e.into_inner());
1660 loop {
1661 {
1662 let q = g.as_mut()?;
1663 if let Some(item) = q.pop_front() {
1664 return Some(item);
1665 }
1666 }
1667 g = self.cv.wait(g).unwrap_or_else(|e| e.into_inner());
1668 }
1669 }
1670
1671 fn close(&self) {
1675 *self.q.lock().unwrap_or_else(|e| e.into_inner()) = None;
1676 self.cv.notify_all();
1677 }
1678}
1679
1680struct DeliveryThread<'a> {
1688 ring: Arc<DeliveryRing>,
1689 inner: &'a Inner,
1690 join: Option<JoinHandle<()>>,
1691}
1692
1693impl Drop for DeliveryThread<'_> {
1694 fn drop(&mut self) {
1695 self.ring.close();
1696 if let Some(j) = self.join.take() {
1697 let _ = j.join();
1698 }
1699 self.inner.deliver_tid.store(0, Ordering::Release);
1700 }
1701}
1702
1703struct BufferPool {
1710 bufs: Mutex<Vec<Vec<u8>>>,
1711 buf_size: usize,
1712}
1713
1714impl BufferPool {
1715 const MAX_POOLED: usize = 16;
1719
1720 fn new(buf_size: usize) -> Self {
1722 Self {
1723 bufs: Mutex::new(Vec::new()),
1724 buf_size,
1725 }
1726 }
1727
1728 #[cfg(test)]
1732 fn take(&self) -> Vec<u8> {
1733 let recycled = self.bufs.lock().unwrap_or_else(|e| e.into_inner()).pop();
1734 match recycled {
1735 Some(v) => self.restore(v),
1736 None => vec![0u8; self.buf_size],
1737 }
1738 }
1739
1740 fn restore(&self, mut v: Vec<u8>) -> Vec<u8> {
1746 debug_assert!(v.capacity() >= self.buf_size);
1747 unsafe { v.set_len(self.buf_size) };
1748 v
1749 }
1750
1751 fn put(&self, v: Vec<u8>) {
1754 if v.capacity() < self.buf_size {
1755 return;
1756 }
1757 let mut g = self.bufs.lock().unwrap_or_else(|e| e.into_inner());
1758 if g.len() < Self::MAX_POOLED {
1759 g.push(v);
1760 }
1761 }
1762
1763 fn drain_into(&self, into: &mut Vec<Vec<u8>>) {
1766 let mut g = self.bufs.lock().unwrap_or_else(|e| e.into_inner());
1767 std::mem::swap(&mut *g, into);
1768 }
1769}
1770
1771struct PoolTaker {
1777 pool: Arc<BufferPool>,
1778 local: Vec<Vec<u8>>,
1779}
1780
1781impl PoolTaker {
1782 fn new(pool: Arc<BufferPool>) -> Self {
1784 Self {
1785 pool,
1786 local: Vec::new(),
1787 }
1788 }
1789
1790 fn take(&mut self) -> Vec<u8> {
1793 if self.local.is_empty() {
1794 self.pool.drain_into(&mut self.local);
1795 }
1796 match self.local.pop() {
1797 Some(v) => self.pool.restore(v),
1798 None => vec![0u8; self.pool.buf_size],
1799 }
1800 }
1801
1802 fn put(&mut self, v: Vec<u8>) {
1804 if v.capacity() >= self.pool.buf_size && self.local.len() < BufferPool::MAX_POOLED {
1805 self.local.push(v);
1806 }
1807 }
1808}
1809
1810struct RunState<'a> {
1814 inner: &'a Inner,
1815 ring: &'a DeliveryRing,
1816 encoder: PcmEncoder,
1817 frame_size_per_channel: usize,
1818 channels: usize,
1819 accum: Vec<i16>,
1822 silence_ref: Vec<i16>,
1826 pcm_fill_bytes: usize,
1827 pool: PoolTaker,
1829 red_history: VecDeque<(Vec<u8>, u64)>,
1832 red_spare: Vec<Vec<u8>>,
1836 red_distance: usize,
1837 sounding: bool,
1842 tail_sounding: bool,
1845 lookahead: usize,
1847 companion: Option<(PcmEncoder, Vec<i16>)>,
1850 companion_bitrate: i32,
1852 total_samples_processed: u64,
1853 ogg: Option<ogg_sink::OggSink>,
1856 ogg_scale: u64,
1857 first_sound_detected: bool,
1858 current_applied_bitrate: i32,
1859 chunks_read: u64,
1860 chunks_silent: u64,
1861 chunks_encoded: u64,
1862 bytes_encoded: u64,
1863}
1864
1865impl<'a> RunState<'a> {
1866 fn feed(&mut self, mut src: &[u8]) {
1874 let chunk_bytes = self.frame_size_per_channel * self.channels * 2;
1875 while !src.is_empty() {
1876 let want = chunk_bytes - self.pcm_fill_bytes;
1877 let take = want.min(src.len());
1878 {
1879 let dst: &mut [u8] = bytemuck::cast_slice_mut(&mut self.accum);
1880 dst[self.pcm_fill_bytes..self.pcm_fill_bytes + take].copy_from_slice(&src[..take]);
1881 }
1882 self.pcm_fill_bytes += take;
1883 src = &src[take..];
1884 if self.pcm_fill_bytes == chunk_bytes {
1885 self.emit_frame();
1886 self.pcm_fill_bytes = 0;
1887 }
1888 }
1889 }
1890
1891 fn emit_frame(&mut self) {
1920 self.chunks_read += 1;
1921
1922 let requested = self.inner.opus_bitrate.load(Ordering::Relaxed);
1923 if requested != self.current_applied_bitrate {
1924 match self.encoder.set_bitrate(requested) {
1925 Ok(()) => {
1926 plog!(
1927 "[pcmflux] Dynamic Bitrate Update: {} -> {} kbps",
1928 self.current_applied_bitrate / 1000,
1929 requested / 1000
1930 );
1931 self.current_applied_bitrate = requested;
1932 }
1933 Err(e) => {
1934 elog!("[pcmflux] Failed to update bitrate ({requested}): {e:?}");
1937 }
1938 }
1939 }
1940
1941 let pts = self.total_samples_processed;
1942 self.total_samples_processed += self.frame_size_per_channel as u64;
1943
1944 let emit_header = self.inner.emit_audio_header.load(Ordering::Relaxed);
1945 let silent =
1946 self.inner.use_silence_gate.load(Ordering::Relaxed) && self.accum == self.silence_ref;
1947 if silent && !(self.sounding && (emit_header || self.tail_sounding)) {
1948 self.sounding = false;
1949 self.chunks_silent += 1;
1950 self.red_spare
1955 .extend(self.red_history.drain(..).map(|(v, _)| v));
1956 return;
1957 }
1958 if !self.first_sound_detected {
1959 plog!("[pcmflux] First non-silent audio chunk detected! Encoding...");
1960 self.first_sound_detected = true;
1961 }
1962
1963 let n = self.frame_size_per_channel * self.channels;
1964 let mut data = self.pool.take();
1965 let prefix = write_ws_prefix_into(
1966 &mut data,
1967 pts,
1968 &self.red_history,
1969 self.red_distance,
1970 emit_header,
1971 );
1972 let encoded = match self.encoder.encode(
1973 &self.accum[..n],
1974 self.frame_size_per_channel,
1975 &mut data[prefix..],
1976 ) {
1977 Ok(b) => b,
1978 Err(e) => {
1979 elog!("[pcmflux] ERROR: {e}");
1980 self.pool.put(data);
1981 return;
1982 }
1983 };
1984 if encoded == 0 {
1985 self.pool.put(data);
1986 return;
1987 }
1988 self.sounding = !silent;
1989 let held = self.lookahead.min(self.frame_size_per_channel) * self.channels;
1990 self.tail_sounding = !silent && self.accum[n - held..n].iter().any(|&v| v != 0);
1991 self.chunks_encoded += 1;
1992 self.bytes_encoded += encoded as u64;
1993 if let Some(sink) = self.ogg.as_mut() {
1994 sink.write_packet(
1995 &data[prefix..prefix + encoded],
1996 self.total_samples_processed * self.ogg_scale,
1997 );
1998 }
1999 if self.red_distance > 0 {
2000 let mut slot = if self.red_history.len() >= self.red_distance {
2001 self.red_history
2002 .pop_front()
2003 .map(|(v, _)| v)
2004 .unwrap_or_default()
2005 } else {
2006 self.red_spare.pop().unwrap_or_default()
2007 };
2008 slot.clear();
2009 slot.extend_from_slice(&data[prefix..prefix + encoded]);
2010 self.red_history.push_back((slot, pts));
2011 }
2012 data.truncate(prefix + encoded);
2013 if silent && emit_header {
2014 self.push_mark(pts);
2015 data[1] |= WS_QUIET;
2016 }
2017
2018 if let Some(data) = self.ring.push(data, pts, self.channels as u8) {
2019 self.pool.put(data);
2020 }
2021 if self.inner.stereo_companion.load(Ordering::Relaxed) {
2022 self.emit_companion(pts, emit_header, silent && emit_header);
2023 }
2024 }
2025
2026 fn emit_quiet(&mut self, pts: u64) {
2032 if std::mem::take(&mut self.sounding)
2033 && self.inner.emit_audio_header.load(Ordering::Relaxed)
2034 {
2035 self.push_mark(pts);
2036 }
2037 }
2038
2039 fn push_mark(&mut self, pts: u64) {
2041 let mut data = self.pool.take();
2042 data[0] = 0x01;
2043 data[1] = WS_QUIET;
2044 data.truncate(2);
2045 if let Some(data) = self.ring.push(data, pts, self.channels as u8) {
2046 self.pool.put(data);
2047 }
2048 }
2049
2050 fn emit_companion(&mut self, pts: u64, emit_header: bool, ending: bool) {
2057 let fpc = self.frame_size_per_channel;
2058 let channels = self.channels;
2059 let bitrate = self.current_applied_bitrate;
2060 let Some((encoder, pcm)) = self.companion.as_mut() else {
2061 return;
2062 };
2063 if self.companion_bitrate != bitrate && encoder.set_bitrate(bitrate).is_ok() {
2064 self.companion_bitrate = bitrate;
2065 }
2066 downmix_stereo(&self.accum[..fpc * channels], channels, pcm);
2067 let mut data = self.pool.take();
2068 let prefix = write_ws_prefix_into(&mut data, pts, &VecDeque::new(), 0, emit_header);
2069 match encoder.encode(pcm, fpc, &mut data[prefix..]) {
2070 Ok(n) if n > 0 => {
2071 data.truncate(prefix + n);
2072 if ending {
2073 data[1] |= WS_QUIET;
2074 }
2075 if let Some(data) = self.ring.push(data, pts, 2) {
2076 self.pool.put(data);
2077 }
2078 }
2079 Ok(_) => self.pool.put(data),
2080 Err(e) => {
2081 elog!("[pcmflux] ERROR: stereo companion: {e}");
2082 self.pool.put(data);
2083 }
2084 }
2085 }
2086}
2087
2088struct PaCaptureSession {
2129 stream: Stream,
2130 #[allow(dead_code)]
2132 context: Context,
2133 mainloop: Mainloop,
2134}
2135
2136enum SessionOpenError {
2138 DeviceNotFound(String),
2141 Transient(String),
2143 Aborted,
2145}
2146
2147fn pa_capture_session_open(
2157 inner: &Inner,
2158 spec: &Spec,
2159 device: Option<&str>,
2160 attr: &BufferAttr,
2161 adjust_latency: bool,
2162 device_was_present: bool,
2163) -> Result<PaCaptureSession, SessionOpenError> {
2164 let tr = |e: &str| SessionOpenError::Transient(e.to_string());
2165 let mut mainloop = match Mainloop::new() {
2166 Some(m) => m,
2167 None => return Err(tr("pa_mainloop_new() failed")),
2168 };
2169 let mut context = match Context::new(&mainloop, "pcmflux") {
2170 Some(c) => c,
2171 None => return Err(tr("pa_context_new() failed")),
2172 };
2173 if context.connect(None, CtxFlags::NOFLAGS, None).is_err() {
2174 return Err(tr("pa_context_connect() failed"));
2175 }
2176 loop {
2177 let st = context.get_state();
2178 if st == pulse::context::State::Ready {
2179 break;
2180 }
2181 if !st.is_good() {
2182 return Err(tr("PulseAudio context connection failed"));
2183 }
2184 if inner.stop_pending() {
2185 return Err(SessionOpenError::Aborted);
2186 }
2187 if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2188 return Err(tr("mainloop iterate failed during connect"));
2189 }
2190 }
2191
2192 if let Some(dev) = device {
2193 let probe = Arc::new(Mutex::new((false, false)));
2194 let p2 = probe.clone();
2195 let op = context
2196 .introspect()
2197 .get_source_info_by_name(dev, move |res| {
2198 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2199 let mut g = p2.lock().unwrap_or_else(|e| e.into_inner());
2200 match res {
2201 ListResult::Item(_) => g.0 = true,
2202 ListResult::End | ListResult::Error => g.1 = true,
2203 }
2204 }));
2205 });
2206 loop {
2207 if probe.lock().unwrap_or_else(|e| e.into_inner()).1 {
2208 break;
2209 }
2210 if inner.stop_pending() {
2211 drop(op);
2212 return Err(SessionOpenError::Aborted);
2213 }
2214 if !context.get_state().is_good() {
2215 drop(op);
2216 return Err(tr("context failed during source probe"));
2217 }
2218 if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2219 drop(op);
2220 return Err(tr("mainloop iterate failed during source probe"));
2221 }
2222 }
2223 drop(op);
2224 if !probe.lock().unwrap_or_else(|e| e.into_inner()).0 {
2225 let msg = format!("PulseAudio source not found: '{dev}'");
2226 return Err(if device_was_present {
2227 SessionOpenError::Transient(msg)
2230 } else {
2231 SessionOpenError::DeviceNotFound(msg)
2232 });
2233 }
2234 }
2235
2236 let map = surround_channel_map(spec.channels as i32)
2237 .and_then(|m| ChannelMap::new_from_string(m).ok());
2238 let mut stream = match Stream::new(&mut context, "Audio Capture", spec, map.as_ref()) {
2239 Some(s) => s,
2240 None => return Err(tr("pa_stream_new() failed")),
2241 };
2242 let flags = if adjust_latency {
2243 StreamFlags::ADJUST_LATENCY
2244 } else {
2245 StreamFlags::NOFLAGS
2246 };
2247 if stream.connect_record(device, Some(attr), flags).is_err() {
2248 return Err(tr("pa_stream_connect_record() failed"));
2249 }
2250 loop {
2251 let st = stream.get_state();
2252 if st == pulse::stream::State::Ready {
2253 break;
2254 }
2255 if !st.is_good() {
2256 return Err(SessionOpenError::Transient(format!(
2257 "PulseAudio record stream failed (device '{}')",
2258 device.unwrap_or("default")
2259 )));
2260 }
2261 if inner.stop_pending() {
2262 return Err(SessionOpenError::Aborted);
2263 }
2264 if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2265 return Err(tr("mainloop iterate failed during stream connect"));
2266 }
2267 }
2268 Ok(PaCaptureSession {
2269 stream,
2270 context,
2271 mainloop,
2272 })
2273}
2274
2275fn capture_run(inner: &Arc<Inner>, settings: &Settings, callback: Option<&Py<PyAny>>) {
2276 inner
2277 .opus_bitrate
2278 .store(settings.opus_bitrate, Ordering::Relaxed);
2279 inner
2280 .use_silence_gate
2281 .store(settings.use_silence_gate, Ordering::Relaxed);
2282 inner
2283 .debug_logging
2284 .store(settings.debug_logging, Ordering::Relaxed);
2285 inner
2286 .emit_audio_header
2287 .store(!settings.omit_audio_header, Ordering::Relaxed);
2288
2289 let spec = Spec {
2291 format: Format::S16le,
2292 rate: settings.sample_rate,
2293 channels: settings.channels as u8,
2294 };
2295
2296 let mut attr = BufferAttr {
2297 maxlength: u32::MAX,
2298 tlength: u32::MAX,
2299 prebuf: u32::MAX,
2300 minreq: u32::MAX,
2301 fragsize: u32::MAX,
2302 };
2303 let adjust_latency = settings.latency_ms > 0;
2304 if adjust_latency {
2305 attr.fragsize = spec.usec_to_bytes(MicroSeconds(settings.latency_ms as u64 * 1000)) as u32;
2306 } else {
2307 attr.fragsize = spec.usec_to_bytes(MicroSeconds(20 * 1000)) as u32;
2308 }
2309
2310 let device = settings.device_name.as_deref();
2311
2312 let encoder = match PcmEncoder::new(
2313 settings.sample_rate,
2314 settings.channels,
2315 settings.use_vbr,
2316 settings.opus_bitrate,
2317 settings.opus_complexity,
2318 ) {
2319 Ok(e) => e,
2320 Err(e) => {
2321 inner.fail(e);
2322 return;
2323 }
2324 };
2325 plog!(
2326 "[pcmflux] SUCCESS: Opus encoder created ({} ch).",
2327 settings.channels
2328 );
2329
2330 let frame_size_per_channel =
2331 (settings.sample_rate as f64 * settings.frame_duration_ms / 1000.0) as usize;
2332 let channels = settings.channels as usize;
2333
2334 let ring = Arc::new(DeliveryRing::new(8));
2335 let max_pkt = multiopus_layout(settings.channels).map_or(1, |(streams, _, _)| streams as usize)
2338 * MAX_OPUS_PACKET;
2339 let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + max_pkt));
2340 let delivery = match callback {
2341 None => {
2342 ring.close();
2343 None
2344 }
2345 Some(callback) => {
2346 let deliver_ring = Arc::clone(&ring);
2347 let deliver_pool = Arc::clone(&pool);
2348 let deliver_inner = Arc::clone(inner);
2349 let deliver_cb: Py<PyAny> = Python::attach(|py| callback.clone_ref(py));
2350 let spawned = std::thread::Builder::new()
2351 .name("pcmflux-deliver".into())
2352 .spawn(move || {
2353 unsafe {
2354 let _ = libc::setpriority(libc::PRIO_PROCESS, gettid() as libc::id_t, -10);
2355 }
2356 deliver_inner.deliver_tid.store(gettid(), Ordering::Release);
2357 while let Some((data, pts, channels)) = deliver_ring.pop() {
2358 Python::attach(|py| {
2359 let frame = match Py::new(
2360 py,
2361 AudioFrame {
2362 data,
2363 pts,
2364 channels,
2365 pool: Some(Arc::clone(&deliver_pool)),
2366 },
2367 ) {
2368 Ok(f) => f,
2369 Err(e) => {
2370 elog!("[pcmflux] AudioFrame alloc failed: {e:?}");
2371 return;
2372 }
2373 };
2374 if let Err(e) = deliver_cb.call1(py, (frame,)) {
2375 e.write_unraisable(py, Some(deliver_cb.bind(py)));
2376 }
2377 });
2378 }
2379 deliver_inner.deliver_tid.store(0, Ordering::Release);
2380 });
2381 match spawned {
2382 Ok(join) => Some(DeliveryThread {
2383 ring: Arc::clone(&ring),
2384 inner,
2385 join: Some(join),
2386 }),
2387 Err(e) => {
2388 inner.fail(format!("delivery thread spawn failed: {e}"));
2389 return;
2390 }
2391 }
2392 }
2393 };
2394
2395 let ogg_scale = 48_000 / settings.sample_rate.max(1) as u64;
2396 let mut encoder = encoder;
2397 let ogg = if settings.output_socket.is_empty() {
2398 None
2399 } else {
2400 match ogg_sink::OggSink::bind(
2401 &settings.output_socket,
2402 &ogg_sink::OpusHead {
2403 channels: channels as u8,
2404 pre_skip: (encoder.lookahead().max(0) as u64 * ogg_scale).min(u16::MAX as u64)
2405 as u16,
2406 input_sample_rate: settings.sample_rate,
2407 mapping: multiopus_layout(channels as i32).map(|(streams, coupled, table)| {
2408 (streams as u8, coupled as u8, table.to_vec())
2409 }),
2410 },
2411 ) {
2412 Ok(sink) => Some(sink),
2413 Err(e) => {
2414 inner.fail(e);
2415 return;
2416 }
2417 }
2418 };
2419 let lookahead = encoder.lookahead().max(0) as usize;
2420 let mut run = RunState {
2421 inner,
2422 ring: &ring,
2423 encoder,
2424 frame_size_per_channel,
2425 channels,
2426 accum: vec![0i16; frame_size_per_channel * channels],
2427 silence_ref: vec![0i16; frame_size_per_channel * channels],
2428 pcm_fill_bytes: 0,
2429 pool: PoolTaker::new(Arc::clone(&pool)),
2430 red_history: VecDeque::new(),
2431 red_spare: Vec::new(),
2432 red_distance: settings.red_distance.max(0) as usize,
2433 sounding: false,
2434 tail_sounding: false,
2435 lookahead,
2436 companion: if channels > 2 {
2437 match PcmEncoder::new(
2438 settings.sample_rate,
2439 2,
2440 settings.use_vbr,
2441 settings.opus_bitrate,
2442 settings.opus_complexity,
2443 ) {
2444 Ok(e) => Some((e, vec![0i16; frame_size_per_channel * 2])),
2445 Err(e) => {
2446 elog!("[pcmflux] stereo companion unavailable: {e}");
2447 None
2448 }
2449 }
2450 } else {
2451 None
2452 },
2453 companion_bitrate: settings.opus_bitrate,
2454 total_samples_processed: 0,
2455 ogg,
2456 ogg_scale,
2457 first_sound_detected: false,
2458 current_applied_bitrate: settings.opus_bitrate,
2459 chunks_read: 0,
2460 chunks_silent: 0,
2461 chunks_encoded: 0,
2462 bytes_encoded: 0,
2463 };
2464
2465 let mut last_log = Instant::now();
2466
2467 const DEVICE_WAIT_TRIES: u32 = 6;
2479 const START_TRIES: u32 = 12;
2480 const RECONNECT_TRIES: u32 = 40;
2481 let mut session: Option<PaCaptureSession> = None;
2482 let mut ever_connected = false;
2483 let mut tries: u32 = 0;
2484 let mut backoff_ms: u64 = 250;
2485 let mut terminal_error: Option<String> = None;
2486
2487 loop {
2488 if inner.stop_pending() {
2489 break;
2490 }
2491 if session.is_none() {
2492 let opened = pa_capture_session_open(
2493 inner,
2494 &spec,
2495 device,
2496 &attr,
2497 adjust_latency,
2498 ever_connected,
2499 );
2500 let cap = match (&opened, ever_connected) {
2501 (Err(SessionOpenError::DeviceNotFound(_)), _) => DEVICE_WAIT_TRIES,
2502 (_, true) => RECONNECT_TRIES,
2503 (_, false) => START_TRIES,
2504 };
2505 match opened {
2506 Ok(s) => {
2507 session = Some(s);
2508 tries = 0;
2509 backoff_ms = 250;
2510 if !ever_connected {
2511 ever_connected = true;
2512 inner.started_ok.store(true, Ordering::Release);
2513 inner.start_state.store(ST_RUNNING, Ordering::Release);
2514 plog!(
2515 "[pcmflux] Capture loop started. Device: {}, Rate: {}, Channels: {}, Bitrate: {} kbps, \
2516 VBR: {}, Silence Gate: {}",
2517 device.unwrap_or("system_default"),
2518 settings.sample_rate,
2519 settings.channels,
2520 settings.opus_bitrate / 1000,
2521 if settings.use_vbr { "On" } else { "Off" },
2522 if settings.use_silence_gate {
2523 "On"
2524 } else {
2525 "Off"
2526 }
2527 );
2528 } else {
2529 plog!("[pcmflux] audio capture reconnected; resuming.");
2530 }
2531 }
2532 Err(SessionOpenError::Aborted) => break,
2533 Err(SessionOpenError::DeviceNotFound(e)) | Err(SessionOpenError::Transient(e)) => {
2534 tries += 1;
2535 if tries >= cap {
2536 terminal_error = Some(e);
2537 break;
2538 }
2539 elog!(
2540 "[pcmflux] audio capture open failed ({e}); retry {tries}/{cap} in {backoff_ms}ms"
2541 );
2542 let mut slept = 0u64;
2543 while slept < backoff_ms && !inner.stop_pending() {
2544 std::thread::sleep(Duration::from_millis(50));
2545 slept += 50;
2546 }
2547 backoff_ms = (backoff_ms * 2).min(5000);
2548 continue;
2549 }
2550 }
2551 }
2552 let s = session.as_mut().expect("session checked above");
2553 if !pump(&mut s.mainloop, PUMP_TIMEOUT_US) {
2554 elog!("[pcmflux] ERROR: mainloop iterate failed; reopening the session.");
2555 session = None;
2556 run.pcm_fill_bytes = 0;
2560 let at = run.total_samples_processed;
2561 run.emit_quiet(at);
2562 continue;
2563 }
2564 let sstate = s.stream.get_state();
2565 if sstate != pulse::stream::State::Ready {
2566 elog!("[pcmflux] record stream lost; reopening the session.");
2567 session = None;
2568 run.pcm_fill_bytes = 0;
2569 let at = run.total_samples_processed;
2570 run.emit_quiet(at);
2571 continue;
2572 }
2573
2574 loop {
2575 let mut discard = false;
2576 let mut done = false;
2577 match s.stream.peek() {
2578 Ok(PeekResult::Empty) => done = true,
2579 Ok(PeekResult::Hole(_)) => discard = true,
2580 Ok(PeekResult::Data(buf)) => {
2581 run.feed(buf);
2582 discard = true;
2583 }
2584 Err(_) => {
2585 elog!("[pcmflux] ERROR: pa_stream_peek() failed.");
2586 done = true;
2587 }
2588 }
2589 if discard {
2590 let _ = s.stream.discard();
2591 }
2592 if done {
2593 break;
2594 }
2595 }
2596
2597 if run.inner.debug_logging.load(Ordering::Relaxed) {
2598 let elapsed = last_log.elapsed();
2599 if elapsed >= Duration::from_secs(2) {
2600 let secs = elapsed.as_secs_f64();
2601 let kbps = (run.bytes_encoded * 8) as f64 / (secs * 1000.0);
2602 let silent_pct = if run.chunks_read > 0 {
2603 100.0 * run.chunks_silent as f64 / run.chunks_read as f64
2604 } else {
2605 0.0
2606 };
2607 plog!(
2608 "[pcmflux] Status | Read: {}, Silent: {} ({:.1}%), Encoded: {}, Rate: {:.2} kbps",
2609 run.chunks_read,
2610 run.chunks_silent,
2611 silent_pct,
2612 run.chunks_encoded,
2613 kbps
2614 );
2615 last_log = Instant::now();
2616 run.chunks_read = 0;
2617 run.chunks_silent = 0;
2618 run.chunks_encoded = 0;
2619 run.bytes_encoded = 0;
2620 }
2621 }
2622 }
2623
2624 if let Some(e) = terminal_error {
2625 inner.fail(format!(
2626 "audio capture could not {} (last error: {e}); stopping.",
2627 if ever_connected {
2628 "stay connected"
2629 } else {
2630 "connect"
2631 }
2632 ));
2633 } else {
2634 plog!("[pcmflux] Stop requested. Cleaning up capture loop...");
2635 inner.started_ok.store(false, Ordering::Release);
2636 inner.start_state.store(ST_IDLE, Ordering::Release);
2639 }
2640 if let Some(s) = session.as_mut() {
2641 let _ = s.stream.disconnect();
2642 }
2643 drop(session);
2644 drop(run);
2645 drop(delivery);
2646 let dropped = ring.dropped.load(Ordering::Relaxed);
2647 if dropped > 0 {
2648 plog!("[pcmflux] Delivery ring dropped {dropped} stale frame(s) to a slow consumer.");
2649 }
2650 plog!("[pcmflux] Audio capture loop finished. Resources released.");
2651}
2652
2653struct PaPlaybackSession {
2657 stream: Stream,
2658 #[allow(dead_code)]
2660 context: Context,
2661 mainloop: Mainloop,
2662}
2663
2664fn pa_playback_session_open(
2672 inner: &Inner,
2673 spec: &Spec,
2674 device: Option<&str>,
2675 attr: &BufferAttr,
2676) -> Result<PaPlaybackSession, SessionOpenError> {
2677 let tr = |e: &str| SessionOpenError::Transient(e.to_string());
2678 let mut mainloop = match Mainloop::new() {
2679 Some(m) => m,
2680 None => return Err(tr("pa_mainloop_new() failed (playback)")),
2681 };
2682 let mut context = match Context::new(&mainloop, "pcmflux") {
2683 Some(c) => c,
2684 None => return Err(tr("pa_context_new() failed (playback)")),
2685 };
2686 if context.connect(None, CtxFlags::NOFLAGS, None).is_err() {
2687 return Err(tr("pa_context_connect() failed (playback)"));
2688 }
2689 loop {
2690 let st = context.get_state();
2691 if st == pulse::context::State::Ready {
2692 break;
2693 }
2694 if !st.is_good() {
2695 return Err(tr("PulseAudio context connection failed (playback)"));
2696 }
2697 if inner.stop_pending() {
2698 return Err(SessionOpenError::Aborted);
2699 }
2700 if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2701 return Err(tr("mainloop iterate failed during connect (playback)"));
2702 }
2703 }
2704
2705 let mut stream = match Stream::new(&mut context, "Microphone Playback", spec, None) {
2706 Some(s) => s,
2707 None => return Err(tr("pa_stream_new() failed (playback)")),
2708 };
2709 if stream
2710 .connect_playback(device, Some(attr), StreamFlags::ADJUST_LATENCY, None, None)
2711 .is_err()
2712 {
2713 return Err(SessionOpenError::Transient(format!(
2714 "pa_stream_connect_playback() failed (device '{}')",
2715 device.unwrap_or("default")
2716 )));
2717 }
2718 loop {
2719 let st = stream.get_state();
2720 if st == pulse::stream::State::Ready {
2721 break;
2722 }
2723 if !st.is_good() {
2724 return Err(SessionOpenError::Transient(format!(
2725 "PulseAudio playback stream failed (device '{}')",
2726 device.unwrap_or("default")
2727 )));
2728 }
2729 if inner.stop_pending() {
2730 return Err(SessionOpenError::Aborted);
2731 }
2732 if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2733 return Err(tr(
2734 "mainloop iterate failed during stream connect (playback)",
2735 ));
2736 }
2737 }
2738 Ok(PaPlaybackSession {
2739 stream,
2740 context,
2741 mainloop,
2742 })
2743}
2744
2745fn playback_run(inner: &Inner, settings: &PbSettings, queue: &PlayQueue) {
2777 inner
2778 .debug_logging
2779 .store(settings.debug_logging, Ordering::Relaxed);
2780
2781 let spec = Spec {
2783 format: Format::S16le,
2784 rate: settings.sample_rate,
2785 channels: settings.channels as u8,
2786 };
2787
2788 let device = settings.device_name.as_deref();
2789 plog!(
2790 "[pcmflux] Attempting to connect playback to PulseAudio device: {} (latency {}ms)",
2791 device.unwrap_or("system_default"),
2792 settings.latency_ms
2793 );
2794
2795 let tlength = spec.usec_to_bytes(MicroSeconds(settings.latency_ms.max(0) as u64 * 1000)) as u32;
2796 let attr = BufferAttr {
2797 maxlength: u32::MAX,
2798 tlength,
2799 prebuf: (tlength / 4).max(spec.frame_size() as u32),
2800 minreq: u32::MAX,
2801 fragsize: u32::MAX,
2802 };
2803
2804 const START_TRIES: u32 = 12;
2807 const RECONNECT_TRIES: u32 = 40;
2808 let mut session: Option<PaPlaybackSession> = None;
2809 let mut ever_connected = false;
2810 let mut tries: u32 = 0;
2811 let mut backoff_ms: u64 = 250;
2812 let mut terminal_error: Option<String> = None;
2813
2814 let mut scratch: Vec<u8> = Vec::new();
2815 let mut bytes_written: u64 = 0;
2816 let mut bytes_cut: u64 = 0;
2817 let mut writable_hits: u64 = 0;
2818 let mut last_pb_log = Instant::now();
2819 let mut standing = StandingDepth::new(spec.bytes_per_second(), spec.frame_size());
2820
2821 loop {
2822 if inner.stop_pending() {
2823 break;
2824 }
2825 if session.is_none() {
2826 match pa_playback_session_open(inner, &spec, device, &attr) {
2827 Ok(s) => {
2828 session = Some(s);
2829 tries = 0;
2830 backoff_ms = 250;
2831 if !ever_connected {
2832 ever_connected = true;
2833 inner.started_ok.store(true, Ordering::Release);
2834 inner.start_state.store(ST_RUNNING, Ordering::Release);
2835 plog!(
2836 "[pcmflux] Playback loop started. Device: {}, Rate: {}, Channels: {}, Latency: {}ms",
2837 device.unwrap_or("system_default"),
2838 settings.sample_rate,
2839 settings.channels,
2840 settings.latency_ms
2841 );
2842 } else {
2843 queue.clear();
2846 standing.reset();
2847 plog!("[pcmflux] audio playback reconnected; resuming.");
2848 }
2849 }
2850 Err(SessionOpenError::Aborted) => break,
2851 Err(SessionOpenError::DeviceNotFound(e)) | Err(SessionOpenError::Transient(e)) => {
2852 tries += 1;
2853 let cap = if ever_connected {
2854 RECONNECT_TRIES
2855 } else {
2856 START_TRIES
2857 };
2858 if tries >= cap {
2859 terminal_error = Some(e);
2860 break;
2861 }
2862 elog!(
2863 "[pcmflux] audio playback open failed ({e}); retry {tries}/{cap} in {backoff_ms}ms"
2864 );
2865 let mut slept = 0u64;
2866 while slept < backoff_ms && !inner.stop_pending() {
2867 std::thread::sleep(Duration::from_millis(50));
2868 slept += 50;
2869 }
2870 backoff_ms = (backoff_ms * 2).min(5000);
2871 continue;
2872 }
2873 }
2874 }
2875 let s = session.as_mut().expect("session checked above");
2876 if !pump(&mut s.mainloop, PUMP_TIMEOUT_US) {
2877 elog!("[pcmflux] ERROR: mainloop iterate failed; reopening the playback session.");
2878 session = None;
2879 continue;
2880 }
2881 if s.stream.get_state() != pulse::stream::State::Ready {
2882 elog!("[pcmflux] playback stream lost; reopening the session.");
2883 session = None;
2884 continue;
2885 }
2886 if let Some(can) = s.stream.writable_size()
2887 && can > 0
2888 {
2889 writable_hits += 1;
2890 let left = queue.drain_upto(can, &mut scratch);
2891 if !scratch.is_empty() {
2892 if let Err(e) = s
2893 .stream
2894 .write(&scratch, None, 0, pulse::stream::SeekMode::Relative)
2895 {
2896 elog!("[pcmflux] ERROR: pa_stream_write() failed: {e:?}");
2897 } else {
2898 bytes_written += scratch.len() as u64;
2899 }
2900 }
2901 if let Some(cut) = standing.after_drain(scratch.len(), left) {
2902 queue.splice_head(cut, standing.keep);
2903 bytes_cut += cut as u64;
2904 }
2905 }
2906 if inner.debug_logging.load(Ordering::Relaxed) && last_pb_log.elapsed().as_secs() >= 1 {
2907 plog!(
2908 "[pcmflux] Playback | writable_hits: {writable_hits}, bytes_written: {bytes_written}, queued: {}, standing depth cut: {bytes_cut}",
2909 queue.buf.lock().map(|q| q.len()).unwrap_or(0)
2910 );
2911 last_pb_log = Instant::now();
2912 }
2913 }
2914
2915 if let Some(e) = terminal_error {
2916 inner.fail(format!(
2917 "audio playback could not {} (last error: {e}); stopping.",
2918 if ever_connected {
2919 "stay connected"
2920 } else {
2921 "connect"
2922 }
2923 ));
2924 } else {
2925 plog!("[pcmflux] Stop requested. Cleaning up playback loop...");
2926 inner.started_ok.store(false, Ordering::Release);
2927 inner.start_state.store(ST_IDLE, Ordering::Release);
2930 }
2931 if let Some(s) = session.as_mut() {
2932 let _ = s.stream.disconnect();
2933 }
2934 drop(session);
2935 plog!("[pcmflux] Audio playback loop finished. Resources released.");
2936}
2937
2938#[pyclass]
2941struct AudioCapture {
2942 shared: Arc<Shared>,
2943}
2944
2945impl AudioCapture {
2946 fn inner(&self) -> &Arc<Inner> {
2947 &self.shared.inner
2948 }
2949}
2950
2951#[pymethods]
2952impl AudioCapture {
2953 #[new]
2954 fn new() -> Self {
2955 AudioCapture {
2956 shared: Arc::new(Shared {
2957 inner: Arc::new(Inner::new()),
2958 thread: Mutex::new(None),
2959 }),
2960 }
2961 }
2962
2963 #[pyo3(signature = (settings, callback = None))]
2987 fn start_capture(
2988 &self,
2989 py: Python<'_>,
2990 settings: &Bound<'_, PyAny>,
2991 callback: Option<Py<PyAny>>,
2992 ) -> PyResult<()> {
2993 let inner = self.inner().clone();
2994
2995 let me = gettid();
2996 if inner.is_own_thread(me) {
2997 inner.undo_self_stop(me);
2998 return Ok(());
2999 }
3000
3001 let parsed = extract_settings(settings)?;
3002
3003 let shared = &self.shared;
3004 let inner_ref = &inner;
3005 let t_inner = inner.clone();
3006 let body = move || capture_run(&t_inner, &parsed, callback.as_ref());
3007 let spawned =
3008 py.detach(move || spawn_worker(&shared.thread, inner_ref, "pcmflux-capture", body));
3009 let my_thread = match spawned {
3010 Some(id) => id,
3011 None => {
3012 return Err(pyo3::exceptions::PyRuntimeError::new_err(
3013 "capture thread spawn failed",
3014 ));
3015 }
3016 };
3017
3018 if let Ok(mut reg) = registry().lock() {
3019 reg.retain(|w| w.strong_count() > 0);
3020 reg.push(Arc::downgrade(&self.shared));
3021 }
3022
3023 await_start(py, &self.shared.thread, &inner, my_thread, "audio capture")
3024 }
3025
3026 fn stop_capture(&self, py: Python<'_>) {
3038 let inner = self.inner();
3039 let me = gettid();
3040 if inner.is_own_thread(me) {
3041 inner.request_self_stop(me);
3042 return;
3043 }
3044 let shared = &self.shared;
3045 py.detach(|| stop_and_join(&shared.thread, inner));
3046 }
3047
3048 fn update_audio_bitrate(&self, bps: i32) {
3052 let clamped = bps.clamp(OPUS_BITRATE_MIN, OPUS_BITRATE_MAX);
3053 self.inner().opus_bitrate.store(clamped, Ordering::Relaxed);
3054 }
3055
3056 fn set_stereo_companion(&self, enabled: bool) {
3062 self.inner()
3063 .stereo_companion
3064 .store(enabled, Ordering::Relaxed);
3065 }
3066
3067 #[getter]
3070 fn is_capturing(&self) -> bool {
3071 self.inner().running()
3072 }
3073
3074 #[getter]
3078 fn state(&self) -> &'static str {
3079 self.inner().state_name()
3080 }
3081
3082 #[getter]
3084 fn last_error(&self) -> Option<String> {
3085 self.inner().last_error()
3086 }
3087}
3088
3089impl Drop for AudioCapture {
3090 fn drop(&mut self) {
3095 let inner = &self.shared.inner;
3096 let me = gettid();
3097 if inner.is_own_thread(me) {
3098 inner.request_self_stop(me);
3099 return;
3100 }
3101 let shared = &self.shared;
3102 Python::attach(|py| py.detach(|| stop_and_join(&shared.thread, inner)));
3103 }
3104}
3105
3106#[pyclass]
3113struct AudioPlayback {
3114 shared: Arc<PbShared>,
3115}
3116
3117impl AudioPlayback {
3118 fn inner(&self) -> &Arc<Inner> {
3119 &self.shared.inner
3120 }
3121
3122 fn decode_packet(
3138 &self,
3139 py: Python<'_>,
3140 data: &Bound<'_, PyAny>,
3141 decode: impl FnOnce(&mut OpusPlaybackDecoder, &[u8], &PlayQueue) + Send,
3142 ) -> PyResult<()> {
3143 if !self.inner().worker_alive() {
3144 return Err(pyo3::exceptions::PyRuntimeError::new_err(
3145 "audio playback is not running (stream failed, stopped, or never started)",
3146 ));
3147 }
3148 let queue = &*self.shared.queue;
3149 if let Ok(b) = data.cast::<PyBytes>() {
3150 let packet = b.as_bytes();
3151 py.detach(|| {
3152 let mut dec = self
3153 .shared
3154 .opus_dec
3155 .lock()
3156 .unwrap_or_else(|e| e.into_inner());
3157 if let Some(d) = dec.as_mut() {
3158 decode(d, packet, queue);
3159 }
3160 });
3161 return Ok(());
3162 }
3163 let buf = PyUntypedBuffer::get(data)?;
3164 if !buf.is_c_contiguous() {
3165 return Err(pyo3::exceptions::PyBufferError::new_err(
3166 "a contiguous bytes-like object is required",
3167 ));
3168 }
3169 let mut dec = self
3170 .shared
3171 .opus_dec
3172 .lock()
3173 .unwrap_or_else(|e| e.into_inner());
3174 let Some(d) = dec.as_mut() else {
3175 return Ok(());
3176 };
3177 let mut packet = std::mem::take(&mut d.packet);
3178 packet.clear();
3179 let n = buf.len_bytes();
3183 if n > 0 {
3184 packet.extend_from_slice(unsafe {
3185 std::slice::from_raw_parts(buf.buf_ptr() as *const u8, n)
3186 });
3187 }
3188 drop(buf);
3189 py.detach(|| decode(d, &packet, queue));
3190 d.packet = packet;
3191 Ok(())
3192 }
3193}
3194
3195#[pymethods]
3196impl AudioPlayback {
3197 #[new]
3198 fn new() -> Self {
3199 AudioPlayback {
3200 shared: Arc::new(PbShared {
3201 inner: Arc::new(Inner::new()),
3202 thread: Mutex::new(None),
3203 queue: Arc::new(PlayQueue::new()),
3204 opus_dec: Mutex::new(None),
3205 }),
3206 }
3207 }
3208
3209 fn start(&self, py: Python<'_>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
3223 let inner = self.inner().clone();
3224
3225 let me = gettid();
3226 if inner.capture_tid.load(Ordering::Acquire) == me {
3227 inner.undo_self_stop(me);
3228 return Ok(());
3229 }
3230
3231 let parsed = extract_pb_settings(settings)?;
3232 let frame_bytes = (parsed.channels.max(1) as usize) * 2;
3233 self.shared
3234 .queue
3235 .configure(parsed.max_buffer_bytes, frame_bytes);
3236
3237 let decoder = OpusPlaybackDecoder::new(parsed.sample_rate, parsed.channels);
3238 if decoder.is_none() {
3239 return Err(pyo3::exceptions::PyRuntimeError::new_err(
3240 "failed to create Opus decoder for playback",
3241 ));
3242 }
3243 *self
3244 .shared
3245 .opus_dec
3246 .lock()
3247 .unwrap_or_else(|e| e.into_inner()) = decoder;
3248
3249 let shared = &self.shared;
3250 let inner_ref = &inner;
3251 let queue = self.shared.queue.clone();
3252 let t_inner = inner.clone();
3253 let body = move || playback_run(&t_inner, &parsed, &queue);
3254 let spawned =
3255 py.detach(move || spawn_worker(&shared.thread, inner_ref, "pcmflux-playback", body));
3256 let my_thread = match spawned {
3257 Some(id) => id,
3258 None => {
3259 return Err(pyo3::exceptions::PyRuntimeError::new_err(
3260 "playback thread spawn failed",
3261 ));
3262 }
3263 };
3264
3265 if let Ok(mut reg) = playback_registry().lock() {
3266 reg.retain(|w| w.strong_count() > 0);
3267 reg.push(Arc::downgrade(&self.shared));
3268 }
3269
3270 await_start(py, &self.shared.thread, &inner, my_thread, "audio playback")
3271 }
3272
3273 fn write(&self, py: Python<'_>, data: &Bound<'_, PyAny>) -> PyResult<()> {
3282 self.decode_packet(py, data, |dec, packet, queue| {
3283 if let Some(pcm) = dec.decode_to_pcm(packet) {
3284 queue.push(pcm);
3285 }
3286 })
3287 }
3288
3289 fn write_red(&self, py: Python<'_>, data: &Bound<'_, PyAny>, primary_ts: i64) -> PyResult<()> {
3298 self.decode_packet(py, data, move |dec, packet, queue| {
3299 dec.decode_red_into_queue(packet, primary_ts, queue);
3300 })
3301 }
3302
3303 fn stop(&self, py: Python<'_>) {
3310 let inner = self.inner();
3311 let me = gettid();
3312 if inner.capture_tid.load(Ordering::Acquire) == me {
3313 inner.request_self_stop(me);
3314 return;
3315 }
3316 let shared = &self.shared;
3317 py.detach(|| stop_and_join(&shared.thread, inner));
3318 }
3319
3320 #[getter]
3323 fn is_running(&self) -> bool {
3324 self.inner().running()
3325 }
3326
3327 #[getter]
3330 fn state(&self) -> &'static str {
3331 self.inner().state_name()
3332 }
3333
3334 #[getter]
3336 fn last_error(&self) -> Option<String> {
3337 self.inner().last_error()
3338 }
3339}
3340
3341impl Drop for AudioPlayback {
3342 fn drop(&mut self) {
3344 let inner = &self.shared.inner;
3345 let me = gettid();
3346 if inner.capture_tid.load(Ordering::Acquire) == me {
3347 inner.request_self_stop(me);
3348 return;
3349 }
3350 let shared = &self.shared;
3351 Python::attach(|py| py.detach(|| stop_and_join(&shared.thread, inner)));
3352 }
3353}
3354
3355#[pyfunction]
3362fn _stop_all_captures(py: Python<'_>) {
3363 let snapshot: Vec<Arc<Shared>> = match registry().lock() {
3364 Ok(reg) => reg.iter().filter_map(|w| w.upgrade()).collect(),
3365 Err(_) => Vec::new(),
3366 };
3367 for shared in snapshot {
3368 py.detach(|| stop_and_join(&shared.thread, &shared.inner));
3369 }
3370 let pb_snapshot: Vec<Arc<PbShared>> = match playback_registry().lock() {
3371 Ok(reg) => reg.iter().filter_map(|w| w.upgrade()).collect(),
3372 Err(_) => Vec::new(),
3373 };
3374 for shared in pb_snapshot {
3375 py.detach(|| stop_and_join(&shared.thread, &shared.inner));
3376 }
3377}
3378
3379#[cfg(test)]
3380mod tests {
3381 use super::*;
3382
3383 #[test]
3388 fn stop_and_join_takes_a_poisoned_lifecycle_lock() {
3389 use std::sync::Arc;
3390 let inner = Arc::new(Inner::new());
3391 let inner_c = inner.clone();
3392 let worker = std::thread::spawn(move || {
3393 while !inner_c.stop_pending() {
3394 std::thread::yield_now();
3395 }
3396 });
3397 let slot = Arc::new(Mutex::new(Some(worker)));
3398 let poisoner = slot.clone();
3399 let _ = std::thread::spawn(move || {
3400 let _guard = poisoner.lock().unwrap();
3401 panic!("poison");
3402 })
3403 .join();
3404 assert!(slot.is_poisoned());
3405 stop_and_join(&slot, &inner);
3406 assert!(slot.lock().unwrap_or_else(|e| e.into_inner()).is_none());
3407 assert_eq!(inner.capture_tid.load(Ordering::Acquire), 0);
3408 }
3409
3410 #[test]
3413 fn red_playback_timestamp_wraparound() {
3414 let f = opus_frames(3);
3415 let mut dec = OpusPlaybackDecoder::new(24000, 1).unwrap();
3416 let q = PlayQueue::new();
3417 q.configure(1 << 20, 2);
3418 let wrap = (u32::MAX as i64) - 100;
3419 dec.decode_red_into_queue(&build_red_payload(&[], &f[0]), wrap, &q);
3420 let next = (wrap.wrapping_add(480)) & 0xFFFF_FFFF;
3423 dec.decode_red_into_queue(&build_red_payload(&[(480, &f[0])], &f[1]), next, &q);
3424 let mut out = Vec::new();
3425 q.drain_upto(1 << 20, &mut out);
3426 assert_eq!(
3427 out.len(),
3428 2 * FRAME_PCM_BYTES,
3429 "frame after the 32-bit wrap was dropped as a duplicate"
3430 );
3431 }
3432
3433 #[test]
3439 fn reentrancy_guard_matches_delivery_thread() {
3440 let inner = Inner::new();
3441 let me = gettid();
3442 assert!(
3443 !inner.is_own_thread(me),
3444 "no run live: nothing should match"
3445 );
3446 inner.deliver_tid.store(me, Ordering::Release);
3447 assert!(
3448 inner.is_own_thread(me),
3449 "delivery tid must short-circuit the guard"
3450 );
3451 inner.deliver_tid.store(0, Ordering::Release);
3452 inner.capture_tid.store(me, Ordering::Release);
3453 assert!(
3454 inner.is_own_thread(me),
3455 "capture tid must still short-circuit the guard"
3456 );
3457 }
3458
3459 fn encode_noise(channels: i32, complexity: Option<i32>) -> Vec<u8> {
3461 let ch = channels as usize;
3462 let frame = 960usize;
3463 let mut enc = PcmEncoder::new(48000, channels, true, 96000, complexity).expect("encoder");
3464 let mut seed = 0x2545_f491u32;
3465 let mut bytes = Vec::new();
3466 for _ in 0..8 {
3467 let pcm: Vec<i16> = (0..frame * ch)
3468 .map(|_| {
3469 seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
3470 ((seed >> 16) as i16) / 4
3471 })
3472 .collect();
3473 let mut out = vec![0u8; 4 * MAX_OPUS_PACKET];
3474 let n = enc.encode(&pcm, frame, &mut out).expect("encode");
3475 bytes.extend_from_slice(&out[..n]);
3476 }
3477 bytes
3478 }
3479
3480 #[test]
3484 fn opus_complexity_reaches_the_encoder() {
3485 for channels in [2, 6] {
3486 let low = encode_noise(channels, Some(0));
3487 let high = encode_noise(channels, Some(10));
3488 assert_ne!(
3489 low, high,
3490 "complexity 0 and 10 encoded identically ({channels} ch)"
3491 );
3492 assert_eq!(
3493 encode_noise(channels, None),
3494 encode_noise(channels, Some(9))
3495 );
3496 }
3497 }
3498
3499 #[test]
3502 fn opus_complexity_is_validated() {
3503 Python::initialize();
3504 Python::attach(|py| {
3505 let settings = Bound::new(py, AudioCaptureSettings::new(py)).unwrap();
3506 let settings = settings.as_any();
3507 assert_eq!(extract_settings(settings).unwrap().opus_complexity, None);
3508 for ok in [0, 5, 10] {
3509 settings.setattr("opus_complexity", ok).unwrap();
3510 assert_eq!(
3511 extract_settings(settings).unwrap().opus_complexity,
3512 Some(ok)
3513 );
3514 }
3515 for bad in [-1, 11] {
3516 settings.setattr("opus_complexity", bad).unwrap();
3517 let err = extract_settings(settings).err().expect("out of range");
3518 assert!(err.is_instance_of::<pyo3::exceptions::PyValueError>(py));
3519 }
3520 });
3521 }
3522
3523 #[test]
3527 fn multiopus_surround_roundtrip() {
3528 let channels = 6usize;
3529 let frame = 480usize;
3530 let mut enc = PcmEncoder::new(48000, channels as i32, true, 256000, None).expect("encoder");
3531 let mut pcm = vec![0i16; frame * channels];
3532 for i in 0..frame {
3533 let v =
3534 (8000.0 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / 48000.0).sin()) as i16;
3535 pcm[i * channels + 2] = v;
3536 }
3537 let mut out = vec![0u8; 4 * MAX_OPUS_PACKET];
3538 let mut n = 0;
3539 for _ in 0..3 {
3540 n = enc.encode(&pcm, frame, &mut out).expect("encode");
3541 }
3542 assert!(n > 0, "surround encode produced no bytes");
3543
3544 unsafe {
3545 let (streams, coupled, mapping) = multiopus_layout(channels as i32).unwrap();
3546 let mut err = 0;
3547 let dec = opusic_sys::opus_multistream_decoder_create(
3548 48000,
3549 channels as i32,
3550 streams,
3551 coupled,
3552 mapping.as_ptr(),
3553 &mut err,
3554 );
3555 assert!(!dec.is_null() && err == 0, "decoder create failed: {err}");
3556 let mut decoded = vec![0i16; frame * channels];
3557 let got = opusic_sys::opus_multistream_decode(
3558 dec,
3559 out.as_ptr(),
3560 n as i32,
3561 decoded.as_mut_ptr(),
3562 frame as i32,
3563 0,
3564 );
3565 opusic_sys::opus_multistream_decoder_destroy(dec);
3566 assert_eq!(got, frame as i32, "decode length mismatch");
3567 let mut rms = vec![0f64; channels];
3568 for i in 0..frame {
3569 for (c, r) in rms.iter_mut().enumerate() {
3570 let s = decoded[i * channels + c] as f64;
3571 *r += s * s;
3572 }
3573 }
3574 let loudest = rms
3575 .iter()
3576 .enumerate()
3577 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3578 .unwrap()
3579 .0;
3580 assert_eq!(loudest, 2, "tone did not come back on FC: rms={rms:?}");
3581 }
3582 }
3583
3584 #[test]
3588 fn downmix_stereo_follows_bs775() {
3589 let one = |ch: usize, pos: usize, v: i16| {
3590 let mut src = vec![0i16; ch];
3591 src[pos] = v;
3592 let mut dst = [0i16; 2];
3593 downmix_stereo(&src, ch, &mut dst);
3594 dst
3595 };
3596 assert_eq!(one(6, 0, 10000), [10000, 0]);
3597 assert_eq!(one(6, 1, 10000), [0, 10000]);
3598 assert_eq!(one(6, 2, 10000), [7071, 7071]);
3599 assert_eq!(one(6, 3, 10000), [0, 0]);
3600 assert_eq!(one(6, 4, 10000), [7071, 0]);
3601 assert_eq!(one(6, 5, 10000), [0, 7071]);
3602 assert_eq!(one(8, 6, 10000), [7071, 0]);
3603 assert_eq!(one(8, 7, 10000), [0, 7071]);
3604 let mut dst = [0i16; 2];
3605 downmix_stereo(&[30000, -30000, 0, 0, 30000, -30000], 6, &mut dst);
3606 assert_eq!(dst, [i16::MAX, i16::MIN]);
3607 }
3608
3609 #[test]
3615 fn stereo_companion_follows_each_surround_frame() {
3616 let inner = Inner::new();
3617 let ring = DeliveryRing::new(64);
3618 let frame = 480usize;
3619 let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + 4 * MAX_OPUS_PACKET));
3620 let mut run = RunState {
3621 inner: &inner,
3622 ring: &ring,
3623 encoder: PcmEncoder::new(48000, 6, true, 256000, None).expect("encoder"),
3624 frame_size_per_channel: frame,
3625 channels: 6,
3626 accum: vec![0i16; frame * 6],
3627 silence_ref: vec![0i16; frame * 6],
3628 pcm_fill_bytes: 0,
3629 pool: PoolTaker::new(Arc::clone(&pool)),
3630 red_history: VecDeque::new(),
3631 red_spare: Vec::new(),
3632 red_distance: 0,
3633 sounding: false,
3634 tail_sounding: false,
3635 lookahead: 120,
3636 companion: Some((
3637 PcmEncoder::new(48000, 2, true, 256000, None).expect("stereo"),
3638 vec![0i16; frame * 2],
3639 )),
3640 companion_bitrate: 256000,
3641 total_samples_processed: 0,
3642 ogg: None,
3643 ogg_scale: 1,
3644 first_sound_detected: true,
3645 current_applied_bitrate: 256000,
3646 chunks_read: 0,
3647 chunks_silent: 0,
3648 chunks_encoded: 0,
3649 bytes_encoded: 0,
3650 };
3651 let mut pcm = vec![0i16; frame * 6];
3652 for i in 0..frame {
3653 pcm[i * 6 + 2] =
3654 ((2.0 * std::f64::consts::PI * 440.0 * i as f64 / 48000.0).sin() * 8000.0) as i16;
3655 }
3656 run.feed(bytemuck::cast_slice(&pcm));
3657 inner.stereo_companion.store(true, Ordering::Relaxed);
3658 for _ in 0..4 {
3659 run.feed(bytemuck::cast_slice(&pcm));
3660 }
3661 let tone_frames = ring.q.lock().unwrap().as_ref().expect("ring open").len();
3662 run.feed(bytemuck::cast_slice(&vec![0i16; frame * 6]));
3663 let queued = ring.q.lock().unwrap();
3664 let frames: Vec<(Vec<u8>, u64, u8)> = queued
3665 .as_ref()
3666 .expect("ring open")
3667 .iter()
3668 .cloned()
3669 .collect();
3670 let tags: Vec<(u64, u8, bool)> = frames
3671 .iter()
3672 .map(|(d, p, c)| (*p, *c, d[1] & WS_QUIET != 0))
3673 .collect();
3674 assert_eq!(
3675 tags,
3676 vec![
3677 (0, 6, false),
3678 (480, 6, false),
3679 (480, 2, false),
3680 (960, 6, false),
3681 (960, 2, false),
3682 (1440, 6, false),
3683 (1440, 2, false),
3684 (1920, 6, false),
3685 (1920, 2, false),
3686 (2400, 6, true),
3687 (2400, 6, true),
3688 (2400, 2, true)
3689 ]
3690 );
3691 let mut dec = OpusPlaybackDecoder::new(48000, 2).expect("decoder");
3692 let mut last = Vec::new();
3693 for (data, _, c) in &frames[..tone_frames] {
3694 if *c == 2 {
3695 last = dec.decode_to_pcm(&data[2..]).expect("decodes").to_vec();
3696 }
3697 }
3698 let samples: &[i16] = bytemuck::cast_slice(&last);
3699 let rms = |ch: usize| {
3700 (samples
3701 .iter()
3702 .skip(ch)
3703 .step_by(2)
3704 .map(|&v| (v as f64).powi(2))
3705 .sum::<f64>()
3706 / frame as f64)
3707 .sqrt()
3708 };
3709 assert!(
3710 rms(0) > 2000.0 && rms(1) > 2000.0,
3711 "center tone on both sides: {} {}",
3712 rms(0),
3713 rms(1)
3714 );
3715 }
3716
3717 #[test]
3720 fn surround_channel_maps_parse() {
3721 use pulse::channelmap::Position;
3722 for channels in [6, 8] {
3723 let map = ChannelMap::new_from_string(surround_channel_map(channels).unwrap())
3724 .expect("parses");
3725 assert_eq!(map.len() as i32, channels);
3726 let pos = map.get();
3727 assert_eq!(
3728 &pos[..4],
3729 &[
3730 Position::FrontLeft,
3731 Position::FrontRight,
3732 Position::FrontCenter,
3733 Position::Lfe
3734 ]
3735 );
3736 }
3737 assert!(surround_channel_map(2).is_none() && surround_channel_map(1).is_none());
3738 }
3739
3740 #[test]
3743 fn opus_durations() {
3744 for ms in [2.5, 5.0, 10.0, 20.0, 40.0, 60.0] {
3745 assert!(valid_opus_duration(ms));
3746 }
3747 for ms in [0.0, 1.0, 3.0, 15.0, 25.0, 30.0, 50.0, 100.0] {
3748 assert!(!valid_opus_duration(ms));
3749 }
3750 }
3751
3752 #[test]
3755 fn opus_sample_rates() {
3756 for rate in [8000, 12000, 16000, 24000, 48000] {
3757 assert!(check_opus_sample_rate(rate).is_ok());
3758 }
3759 for rate in [0, 11025, 22050, 44100, 96000] {
3760 assert!(check_opus_sample_rate(rate).is_err());
3761 }
3762 }
3763
3764 #[test]
3768 fn frame_geometry() {
3769 let fspc = (48000usize * 20) / 1000;
3770 assert_eq!(fspc, 960);
3771 assert_eq!(fspc * 2 * 2, 3840);
3772 let m = (24000usize * 10) / 1000;
3773 assert_eq!(m, 240);
3774 assert_eq!(m * 2, 480);
3775 }
3776
3777 #[test]
3780 fn red_zero_is_byte_identical() {
3781 let opus = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42];
3782 let hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3783
3784 let legacy_expected = {
3785 let mut v = vec![0x01u8, 0x00];
3786 v.extend_from_slice(&opus);
3787 v
3788 };
3789 assert_eq!(build_ws_body(&opus, 4096, &hist, 0, true), legacy_expected);
3790 assert_eq!(build_ws_body(&opus, 4096, &hist, 0, false), opus);
3791 }
3792
3793 #[test]
3797 fn red_two_roundtrips() {
3798 let f_n2 = vec![0xA0, 0xA1, 0xA2];
3799 let f_n1 = vec![0xB0, 0xB1, 0xB2, 0xB3];
3800 let primary = vec![0xC0, 0xC1];
3801 let mut hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3802 hist.push_back((f_n2.clone(), 0));
3803 hist.push_back((f_n1.clone(), 960));
3804
3805 let body = build_ws_body(&primary, 1920, &hist, 2, true);
3806 assert_eq!(body[0], 0x01);
3807 let n_red = body[1] as usize;
3808 assert_eq!(n_red, 2);
3809 assert_eq!(
3810 u32::from_be_bytes([body[2], body[3], body[4], body[5]]),
3811 1920
3812 );
3813
3814 let mut idx = 6;
3815 let mut offsets = Vec::new();
3816 let mut lens = Vec::new();
3817 for _ in 0..n_red {
3818 assert_eq!(body[idx] & 0x80, 0x80, "redundant header F bit must be set");
3819 let word = ((body[idx + 1] as u32) << 16)
3820 | ((body[idx + 2] as u32) << 8)
3821 | (body[idx + 3] as u32);
3822 offsets.push((word >> 10) & 0x3FFF);
3823 lens.push((word & 0x3FF) as usize);
3824 idx += 4;
3825 }
3826 assert_eq!(body[idx] & 0x80, 0x00, "primary header F bit must be clear");
3827 idx += 1;
3828
3829 assert_eq!(offsets, vec![1920, 960]);
3830 assert_eq!(lens, vec![f_n2.len(), f_n1.len()]);
3831
3832 let b0 = &body[idx..idx + lens[0]];
3833 idx += lens[0];
3834 let b1 = &body[idx..idx + lens[1]];
3835 idx += lens[1];
3836 let prim = &body[idx..];
3837 assert_eq!(b0, &f_n2[..]);
3838 assert_eq!(b1, &f_n1[..]);
3839 assert_eq!(prim, &primary[..]);
3840 }
3841
3842 fn build_red_payload(reds: &[(u64, &[u8])], primary: &[u8]) -> Vec<u8> {
3847 let mut v = Vec::new();
3848 for (off, blk) in reds {
3849 let field = (((*off as u32) & 0x3FFF) << 10) | ((blk.len() as u32) & 0x3FF);
3850 v.push(0x80 | (RED_BLOCK_PT & 0x7F));
3851 v.push((field >> 16) as u8);
3852 v.push((field >> 8) as u8);
3853 v.push(field as u8);
3854 }
3855 v.push(RED_BLOCK_PT & 0x7F);
3856 for (_, blk) in reds {
3857 v.extend_from_slice(blk);
3858 }
3859 v.extend_from_slice(primary);
3860 v
3861 }
3862
3863 fn opus_frames(n: usize) -> Vec<Vec<u8>> {
3866 let mut enc = PcmEncoder::new(24000, 1, true, 24000, None).unwrap();
3867 (0..n)
3868 .map(|s| {
3869 let pcm: Vec<i16> = (0..480)
3870 .map(|k| ((k * (s as i32 + 1)) % 4000 - 2000) as i16)
3871 .collect();
3872 let mut out = vec![0u8; 4000];
3873 let len = enc.encode(&pcm, 480, &mut out).unwrap();
3874 out.truncate(len);
3875 out
3876 })
3877 .collect()
3878 }
3879
3880 const FRAME_PCM_BYTES: usize = 480 * 2;
3882
3883 #[test]
3890 fn red_playback_recovers_lost_frame() {
3891 let f = opus_frames(3);
3892 let mut dec = OpusPlaybackDecoder::new(24000, 1).unwrap();
3893 let q = PlayQueue::new();
3894 q.configure(1 << 20, 2);
3895
3896 dec.decode_red_into_queue(&build_red_payload(&[], &f[0]), 1000, &q);
3897 let pkt3 = build_red_payload(&[(960, &f[0]), (480, &f[1])], &f[2]);
3898 dec.decode_red_into_queue(&pkt3, 1960, &q);
3899
3900 let mut out = Vec::new();
3901 q.drain_upto(1 << 20, &mut out);
3902 assert_eq!(out.len(), 3 * FRAME_PCM_BYTES);
3903 assert_eq!(dec.last_ts, Some(1960));
3904 }
3905
3906 #[test]
3909 fn red_playback_no_double_decode() {
3910 let f = opus_frames(3);
3911 let mut dec = OpusPlaybackDecoder::new(24000, 1).unwrap();
3912 let q = PlayQueue::new();
3913 q.configure(1 << 20, 2);
3914
3915 dec.decode_red_into_queue(&build_red_payload(&[], &f[0]), 1000, &q);
3916 dec.decode_red_into_queue(&build_red_payload(&[(480, &f[0])], &f[1]), 1480, &q);
3917 dec.decode_red_into_queue(&build_red_payload(&[(480, &f[1])], &f[2]), 1960, &q);
3918
3919 let mut out = Vec::new();
3920 q.drain_upto(1 << 20, &mut out);
3921 assert_eq!(out.len(), 3 * FRAME_PCM_BYTES);
3922 assert_eq!(dec.last_ts, Some(1960));
3923 }
3924
3925 #[test]
3929 fn red_skips_oversize_and_too_old() {
3930 let too_old = vec![0x11u8; 3];
3931 let oversize = vec![0x22u8; 1100];
3932 let good = vec![0x33u8; 5];
3933 let primary = vec![0x44u8; 2];
3934 let primary_pts = 100_000u64;
3935
3936 let mut hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3937 hist.push_back((too_old, primary_pts - 20_000));
3938 hist.push_back((oversize, primary_pts - 1920));
3939 hist.push_back((good.clone(), primary_pts - 960));
3940
3941 let body = build_ws_body(&primary, primary_pts, &hist, 4, true);
3942 assert_eq!(body[0], 0x01);
3943 assert_eq!(body[1], 1, "only the in-range block survives");
3944 assert_eq!(
3945 u32::from_be_bytes([body[2], body[3], body[4], body[5]]),
3946 primary_pts as u32
3947 );
3948
3949 let word = ((body[7] as u32) << 16) | ((body[8] as u32) << 8) | (body[9] as u32);
3950 assert_eq!((word >> 10) & 0x3FFF, 960);
3951 let len = (word & 0x3FF) as usize;
3952 assert_eq!(len, good.len());
3953 assert_eq!(
3954 body[10] & 0x80,
3955 0x00,
3956 "primary header follows the one redundant header"
3957 );
3958 assert_eq!(&body[11..11 + len], &good[..]);
3959 assert_eq!(&body[11 + len..], &primary[..]);
3960 }
3961
3962 #[test]
3969 fn red_empty_history_collapses_to_bare_header() {
3970 let opus = vec![0x77u8; 4];
3971 let hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3972 let body = build_ws_body(&opus, 960, &hist, 2, true);
3973 assert_eq!(body[0], 0x01);
3974 assert_eq!(body[1], 0x00);
3975 assert_eq!(&body[2..], &opus[..]);
3976 assert_eq!(body.len(), 2 + opus.len());
3977 }
3978
3979 #[test]
3987 fn gated_silence_sends_one_quiet_frame() {
3988 for (red, header) in [(0usize, true), (2, true), (0, false)] {
3989 let inner = Inner::new();
3990 inner.emit_audio_header.store(header, Ordering::Relaxed);
3991 let ring = DeliveryRing::new(64);
3992 let frame = 480usize;
3993 let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
3994 let mut run = RunState {
3995 inner: &inner,
3996 ring: &ring,
3997 encoder: PcmEncoder::new(48000, 2, true, 64000, None).expect("encoder"),
3998 frame_size_per_channel: frame,
3999 channels: 2,
4000 accum: vec![0i16; frame * 2],
4001 silence_ref: vec![0i16; frame * 2],
4002 pcm_fill_bytes: 0,
4003 pool: PoolTaker::new(Arc::clone(&pool)),
4004 red_history: VecDeque::new(),
4005 red_spare: Vec::new(),
4006 red_distance: red,
4007 sounding: false,
4008 tail_sounding: false,
4009 lookahead: 120,
4010 companion: None,
4011 companion_bitrate: 64000,
4012 total_samples_processed: 0,
4013 ogg: None,
4014 ogg_scale: 1,
4015 first_sound_detected: true,
4016 current_applied_bitrate: 64000,
4017 chunks_read: 0,
4018 chunks_silent: 0,
4019 chunks_encoded: 0,
4020 bytes_encoded: 0,
4021 };
4022 let tone: Vec<i16> = (0..frame * 2)
4023 .map(|i| ((i as f64 * 0.3).sin() * 8000.0) as i16)
4024 .collect();
4025 let quiet = vec![0i16; frame * 2];
4026 for pcm in [&quiet, &tone, &tone, &quiet, &quiet, &quiet, &tone, &tone] {
4027 run.feed(bytemuck::cast_slice(pcm));
4028 }
4029 let queued = ring.q.lock().unwrap();
4030 let frames = queued.as_ref().expect("ring open");
4031 let pts: Vec<u64> = frames.iter().map(|(_, p, _)| *p).collect();
4032 let quiet_frames: Vec<&Vec<u8>> = frames
4033 .iter()
4034 .map(|(d, _, _)| d)
4035 .filter(|d| d.len() == 2)
4036 .collect();
4037 if header {
4038 assert_eq!(pts, vec![480, 960, 1440, 1440, 2880, 3360], "red={red}");
4039 assert_eq!(quiet_frames, vec![&vec![0x01, WS_QUIET]], "red={red}");
4040 assert_eq!(
4041 frames[2].0.len(),
4042 2,
4043 "the bare mark goes where the sound ends"
4044 );
4045 assert!(
4046 frames[3].0.len() > 2 && frames[3].0[1] & WS_QUIET != 0,
4047 "the silent frame behind it carries the mark too"
4048 );
4049 assert!(
4050 frames
4051 .iter()
4052 .enumerate()
4053 .all(|(i, (d, _, _))| i == 2 || i == 3 || d[1] & WS_QUIET == 0)
4054 );
4055 assert_eq!(frames[4].0[1], 0x00, "sound resumes with the plain header");
4056 } else {
4057 assert_eq!(pts, vec![480, 960, 1440, 2880, 3360]);
4058 assert!(quiet_frames.is_empty() && frames.iter().all(|(d, _, _)| d.len() > 2));
4059 }
4060 }
4061 }
4062
4063 #[test]
4067 fn a_header_capture_sends_the_tail_of_a_sound_before_the_gate() {
4068 let inner = Inner::new();
4069 inner.emit_audio_header.store(true, Ordering::Relaxed);
4070 let ring = DeliveryRing::new(64);
4071 let frame = 480usize;
4072 let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4073 let mut run = RunState {
4074 inner: &inner,
4075 ring: &ring,
4076 encoder: PcmEncoder::new(48000, 2, true, 128000, None).expect("encoder"),
4077 frame_size_per_channel: frame,
4078 channels: 2,
4079 accum: vec![0i16; frame * 2],
4080 silence_ref: vec![0i16; frame * 2],
4081 pcm_fill_bytes: 0,
4082 pool: PoolTaker::new(Arc::clone(&pool)),
4083 red_history: VecDeque::new(),
4084 red_spare: Vec::new(),
4085 red_distance: 0,
4086 sounding: false,
4087 tail_sounding: false,
4088 lookahead: 120,
4089 companion: None,
4090 companion_bitrate: 128000,
4091 total_samples_processed: 0,
4092 ogg: None,
4093 ogg_scale: 1,
4094 first_sound_detected: true,
4095 current_applied_bitrate: 128000,
4096 chunks_read: 0,
4097 chunks_silent: 0,
4098 chunks_encoded: 0,
4099 bytes_encoded: 0,
4100 };
4101 let mut click = vec![0i16; frame * 2];
4102 for i in frame - 96..frame {
4103 let v =
4104 ((i as f64 * 2.0 * std::f64::consts::PI * 1500.0 / 48000.0).sin() * 16000.0) as i16;
4105 click[i * 2] = v;
4106 click[i * 2 + 1] = v;
4107 }
4108 let quiet = vec![0i16; frame * 2];
4109 for pcm in [&quiet, &click, &quiet, &quiet, &quiet] {
4110 run.feed(bytemuck::cast_slice(pcm));
4111 }
4112 let queued = ring.q.lock().unwrap();
4113 let marked: Vec<(u64, usize)> = queued
4114 .as_ref()
4115 .expect("ring open")
4116 .iter()
4117 .filter(|(d, _, _)| d[1] & WS_QUIET != 0)
4118 .map(|(d, p, _)| (*p, d.len().min(3)))
4119 .collect();
4120 assert_eq!(
4121 marked,
4122 vec![(960, 2), (960, 3)],
4123 "the bare mark, then the frame that ends the sound"
4124 );
4125 let mut decoder = OpusPlaybackDecoder::new(48000, 2).expect("decoder");
4126 let mut peak = 0i32;
4127 for (data, _, _) in queued.as_ref().expect("ring open") {
4128 if data.len() <= 2 {
4129 continue;
4130 }
4131 let pcm: &[i16] =
4132 bytemuck::cast_slice(decoder.decode_to_pcm(&data[2..]).expect("decode"));
4133 peak = peak.max(pcm.iter().map(|&v| (v as i32).abs()).max().unwrap_or(0));
4134 }
4135 assert!(peak > 8000, "the sound's tail decodes at {peak}");
4136 }
4137
4138 #[test]
4143 fn a_raw_capture_sends_the_tail_only_when_the_sound_reached_the_lookahead() {
4144 for late in [true, false] {
4145 let inner = Inner::new();
4146 inner.emit_audio_header.store(false, Ordering::Relaxed);
4147 let ring = DeliveryRing::new(64);
4148 let frame = 480usize;
4149 let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4150 let mut run = RunState {
4151 inner: &inner,
4152 ring: &ring,
4153 encoder: PcmEncoder::new(48000, 2, true, 128000, None).expect("encoder"),
4154 frame_size_per_channel: frame,
4155 channels: 2,
4156 accum: vec![0i16; frame * 2],
4157 silence_ref: vec![0i16; frame * 2],
4158 pcm_fill_bytes: 0,
4159 pool: PoolTaker::new(Arc::clone(&pool)),
4160 red_history: VecDeque::new(),
4161 red_spare: Vec::new(),
4162 red_distance: 0,
4163 sounding: false,
4164 tail_sounding: false,
4165 lookahead: 120,
4166 companion: None,
4167 companion_bitrate: 128000,
4168 total_samples_processed: 0,
4169 ogg: None,
4170 ogg_scale: 1,
4171 first_sound_detected: true,
4172 current_applied_bitrate: 128000,
4173 chunks_read: 0,
4174 chunks_silent: 0,
4175 chunks_encoded: 0,
4176 bytes_encoded: 0,
4177 };
4178 let mut click = vec![0i16; frame * 2];
4179 let burst = if late { frame - 96..frame } else { 96..192 };
4180 for i in burst {
4181 let v = ((i as f64 * 2.0 * std::f64::consts::PI * 1500.0 / 48000.0).sin() * 16000.0)
4182 as i16;
4183 click[i * 2] = v;
4184 click[i * 2 + 1] = v;
4185 }
4186 let quiet = vec![0i16; frame * 2];
4187 for pcm in [&quiet, &click, &quiet, &quiet, &quiet] {
4188 run.feed(bytemuck::cast_slice(pcm));
4189 }
4190 let queued = ring.q.lock().unwrap();
4191 let frames = queued.as_ref().expect("ring open");
4192 let pts: Vec<u64> = frames.iter().map(|(_, p, _)| *p).collect();
4193 if !late {
4194 assert_eq!(
4195 pts,
4196 vec![480],
4197 "a burst that ended before the lookahead needs no tail"
4198 );
4199 continue;
4200 }
4201 assert_eq!(
4202 pts,
4203 vec![480, 960],
4204 "the silent frame carrying the burst's end follows it"
4205 );
4206 let mut decoder = OpusPlaybackDecoder::new(48000, 2).expect("decoder");
4207 let mut peak = 0i32;
4208 for (data, _, _) in frames {
4209 let pcm: &[i16] =
4210 bytemuck::cast_slice(decoder.decode_to_pcm(data).expect("decode"));
4211 peak = peak.max(pcm.iter().map(|&v| (v as i32).abs()).max().unwrap_or(0));
4212 }
4213 assert!(peak > 8000, "the burst decodes at {peak}");
4214 }
4215 }
4216
4217 #[test]
4221 fn a_dropped_session_after_sound_sends_the_bare_mark_once() {
4222 for header in [true, false] {
4223 let inner = Inner::new();
4224 inner.emit_audio_header.store(header, Ordering::Relaxed);
4225 let ring = DeliveryRing::new(64);
4226 let frame = 480usize;
4227 let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4228 let mut run = RunState {
4229 inner: &inner,
4230 ring: &ring,
4231 encoder: PcmEncoder::new(48000, 2, true, 64000, None).expect("encoder"),
4232 frame_size_per_channel: frame,
4233 channels: 2,
4234 accum: vec![0i16; frame * 2],
4235 silence_ref: vec![0i16; frame * 2],
4236 pcm_fill_bytes: 0,
4237 pool: PoolTaker::new(Arc::clone(&pool)),
4238 red_history: VecDeque::new(),
4239 red_spare: Vec::new(),
4240 red_distance: 0,
4241 sounding: false,
4242 tail_sounding: false,
4243 lookahead: 120,
4244 companion: None,
4245 companion_bitrate: 64000,
4246 total_samples_processed: 0,
4247 ogg: None,
4248 ogg_scale: 1,
4249 first_sound_detected: true,
4250 current_applied_bitrate: 64000,
4251 chunks_read: 0,
4252 chunks_silent: 0,
4253 chunks_encoded: 0,
4254 bytes_encoded: 0,
4255 };
4256 run.emit_quiet(0);
4257 let tone: Vec<i16> = (0..frame * 2)
4258 .map(|i| ((i as f64 * 0.3).sin() * 8000.0) as i16)
4259 .collect();
4260 run.feed(bytemuck::cast_slice(&tone));
4261 run.emit_quiet(480);
4262 run.emit_quiet(480);
4263 let queued = ring.q.lock().unwrap();
4264 let bare: Vec<&Vec<u8>> = queued
4265 .as_ref()
4266 .expect("ring open")
4267 .iter()
4268 .map(|(d, _, _)| d)
4269 .filter(|d| d.len() == 2)
4270 .collect();
4271 if header {
4272 assert_eq!(bare, vec![&vec![0x01, WS_QUIET]]);
4273 } else {
4274 assert!(bare.is_empty());
4275 }
4276 }
4277 }
4278
4279 #[test]
4282 fn silence_detection() {
4283 let silent = vec![0i16; 960 * 2];
4284 assert!(silent.iter().all(|&s| s == 0));
4285 let mut not = silent.clone();
4286 not[123] = 7;
4287 assert!(!not.iter().all(|&s| s == 0));
4288 }
4289
4290 #[test]
4299 fn external_stop_never_lost_to_self_restart() {
4300 use std::sync::Arc;
4301 let me: i64 = 987654;
4302 for _ in 0..5000 {
4303 let inner = Arc::new(Inner::new());
4304 let inner_c = inner.clone();
4305 let observed = Arc::new(AtomicBool::new(false));
4306 let observed_c = observed.clone();
4307 let h = std::thread::spawn(move || {
4308 let mut spins: u64 = 0;
4309 loop {
4310 inner_c.request_self_stop(me);
4311 inner_c.undo_self_stop(me);
4312 if inner_c.stop_pending()
4313 && inner_c.stop_state.load(Ordering::Acquire) == STOP_EXTERNAL
4314 {
4315 observed_c.store(true, Ordering::Release);
4316 return;
4317 }
4318 spins += 1;
4319 assert!(
4320 spins < 50_000_000,
4321 "external stop was lost (would hang the join)"
4322 );
4323 }
4324 });
4325 inner.request_external_stop();
4326 h.join().unwrap();
4327 assert!(observed.load(Ordering::Acquire));
4328 assert_eq!(inner.stop_state.load(Ordering::Acquire), STOP_EXTERNAL);
4329 assert!(inner.stop_pending());
4330 }
4331 }
4332
4333 #[test]
4336 fn playqueue_drop_oldest_keeps_newest() {
4337 let q = PlayQueue::new();
4338 q.configure(8, 2);
4339 q.push(&[1, 2, 3, 4, 5, 6]);
4340 q.push(&[7, 8, 9, 10, 11, 12]);
4341 let mut out = Vec::new();
4342 q.drain_upto(100, &mut out);
4343 assert_eq!(out, vec![5, 6, 7, 8, 9, 10, 11, 12]);
4344 q.drain_upto(100, &mut out);
4345 assert!(out.is_empty());
4346 }
4347
4348 #[test]
4353 fn playqueue_drain_is_frame_aligned() {
4354 let q = PlayQueue::new();
4355 q.configure(1000, 4);
4356 q.push(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
4357 let mut out = Vec::new();
4358 q.drain_upto(7, &mut out);
4359 assert_eq!(out, vec![0, 1, 2, 3]);
4360 q.drain_upto(100, &mut out);
4361 assert_eq!(out, vec![4, 5, 6, 7]);
4362 q.drain_upto(100, &mut out);
4363 assert!(out.is_empty());
4364 }
4365
4366 #[test]
4369 fn playqueue_configure_resets() {
4370 let q = PlayQueue::new();
4371 q.push(&[1, 2, 3, 4]);
4372 q.configure(96000, 2);
4373 let mut out = Vec::new();
4374 q.drain_upto(100, &mut out);
4375 assert!(out.is_empty(), "configure must clear stale audio");
4376 }
4377
4378 #[test]
4383 fn playqueue_misaligned_bound_never_splits_frames() {
4384 let q = PlayQueue::new();
4385 q.configure(9, 4);
4387 q.push(&[0, 1, 2, 3]);
4388 q.push(&[4, 5, 6, 7]);
4389 q.push(&[8, 9, 10, 11]);
4391 let mut out = Vec::new();
4392 q.drain_upto(100, &mut out);
4393 assert_eq!(out, vec![4, 5, 6, 7, 8, 9, 10, 11]);
4394 }
4395
4396 #[test]
4400 fn playqueue_splice_crossfades_the_seam() {
4401 let q = PlayQueue::new();
4402 q.configure(1 << 20, 2);
4403 let head: Vec<i16> = vec![1000; 48];
4404 let tail: Vec<i16> = (0..200).map(|i| -2000 + i).collect();
4405 q.push(bytemuck::cast_slice(&head));
4406 q.push(bytemuck::cast_slice(&[0i16; 52]));
4407 q.push(bytemuck::cast_slice(&tail));
4408 q.splice_head(200, 96);
4409 let mut out = Vec::new();
4410 assert_eq!(q.drain_upto(1 << 20, &mut out), 0);
4411 let got: Vec<i16> = bytemuck::cast_slice::<u8, i16>(&out).to_vec();
4412 assert_eq!(got.len(), 200);
4413 assert!(
4414 (got[0] - 1000).abs() < 70,
4415 "first sample stays near the head: {}",
4416 got[0]
4417 );
4418 assert!(
4419 (got[47] - tail[47]).abs() < 70,
4420 "last blended sample reaches the tail: {}",
4421 got[47]
4422 );
4423 assert!(
4424 got.windows(2).take(48).all(|w| (w[1] - w[0]).abs() < 70),
4425 "no step inside the fade"
4426 );
4427 assert_eq!(&got[48..], &tail[48..]);
4428
4429 let short = PlayQueue::new();
4430 short.configure(1 << 20, 2);
4431 short.push(&[1, 0, 2, 0, 3, 0]);
4432 short.splice_head(4, 4);
4433 let mut out = Vec::new();
4434 short.drain_upto(100, &mut out);
4435 assert_eq!(out, vec![1, 0, 2, 0, 3, 0]);
4436 }
4437
4438 #[test]
4445 fn standing_depth_is_shed_after_bursts_and_drift() {
4446 fn run(stall: bool, skew: f64, seconds: f64, cut: bool) -> (usize, usize) {
4447 let (rate, fb) = (24_000usize, 2usize);
4448 let q = PlayQueue::new();
4449 q.configure(96_000, fb);
4450 let mut standing = StandingDepth::new(rate * fb, fb);
4451 let packet = vec![0u8; 960];
4452 let mut out = Vec::new();
4453 let (mut t, mut next_arrival, mut k) = (0.0f64, 0.0f64, 0u64);
4454 let (mut worst_late, mut end) = (0usize, 0usize);
4455 let mut held = 0u64;
4456 while t < seconds {
4457 while next_arrival <= t {
4458 let in_stall = stall && (5.0..5.5).contains(&next_arrival);
4459 if in_stall {
4460 held += 1;
4461 } else {
4462 for _ in 0..=held {
4463 q.push(&packet);
4464 }
4465 held = 0;
4466 }
4467 k += 1;
4468 next_arrival = k as f64 * 0.020 / (1.0 + skew);
4469 }
4470 let left = q.drain_upto(480, &mut out);
4471 if cut && let Some(n) = standing.after_drain(out.len(), left) {
4472 q.splice_head(n, standing.keep);
4473 }
4474 let depth = q.buf.lock().unwrap().len();
4475 if t > 8.0 {
4476 worst_late = worst_late.max(depth);
4477 }
4478 end = depth;
4479 t += 0.010;
4480 }
4481 (worst_late, end)
4482 }
4483 let (late, _) = run(true, 0.0, 30.0, true);
4484 assert!(
4485 late <= 960 + 480,
4486 "burst shed by 8 s: {late} bytes still standing"
4487 );
4488 let (_, held) = run(true, 0.0, 30.0, false);
4489 assert!(held >= 20_000, "without the cut the burst stands: {held}");
4490 let (drift, _) = run(false, 0.001, 600.0, true);
4491 assert!(drift <= 480 + 960 + 96, "drift bounded: {drift} bytes");
4492 let (_, grown) = run(false, 0.001, 600.0, false);
4493 assert!(grown >= 25_000, "without the cut drift grows: {grown}");
4494 }
4495
4496 #[test]
4502 fn worker_alive_tracks_loop_error_exit() {
4503 let inner = Inner::new();
4504 assert!(!inner.worker_alive(), "no worker yet");
4505
4506 inner.start_state.store(ST_STARTING, Ordering::Release);
4507 assert!(inner.worker_alive(), "startup handshake counts as alive");
4508
4509 inner.started_ok.store(true, Ordering::Release);
4510 inner.start_state.store(ST_RUNNING, Ordering::Release);
4511 assert!(inner.worker_alive());
4512
4513 inner.started_ok.store(false, Ordering::Release);
4514 assert_eq!(inner.stop_state.load(Ordering::Acquire), STOP_NONE);
4515 assert!(!inner.worker_alive(), "dead loop must be observable");
4516
4517 inner.started_ok.store(true, Ordering::Release);
4518 inner.request_external_stop();
4519 assert!(!inner.worker_alive());
4520 }
4521
4522 #[test]
4527 fn state_and_last_error_track_lifecycle() {
4528 let inner = Arc::new(Inner::new());
4529 assert_eq!(inner.state_name(), "idle");
4530 assert_eq!(inner.last_error(), None);
4531
4532 inner.start_state.store(ST_STARTING, Ordering::Release);
4533 assert_eq!(inner.state_name(), "starting");
4534 assert!(!inner.running());
4535
4536 inner.started_ok.store(true, Ordering::Release);
4537 inner.start_state.store(ST_RUNNING, Ordering::Release);
4538 assert_eq!(inner.state_name(), "running");
4539 assert!(inner.running());
4540
4541 inner.request_external_stop();
4542 assert_eq!(
4543 inner.state_name(),
4544 "idle",
4545 "a pending stop is no longer running"
4546 );
4547 inner.clear_stop();
4548
4549 inner.fail("source vanished".to_string());
4550 assert_eq!(inner.state_name(), "failed");
4551 assert_eq!(inner.last_error().as_deref(), Some("source vanished"));
4552 assert!(!inner.running());
4553 assert!(!inner.worker_alive());
4554
4555 let slot: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
4556 let wi = inner.clone();
4557 spawn_worker(&slot, &inner, "fresh", move || {
4558 wi.started_ok.store(false, Ordering::Release);
4559 wi.start_state.store(ST_IDLE, Ordering::Release);
4560 })
4561 .unwrap();
4562 slot.lock().unwrap().take().unwrap().join().unwrap();
4563 assert_eq!(
4564 inner.last_error(),
4565 None,
4566 "a new run must start with no stale error"
4567 );
4568 assert_eq!(inner.state_name(), "idle", "a clean stop reads idle again");
4569 }
4570
4571 #[test]
4575 fn worker_panic_records_last_error() {
4576 let inner = Arc::new(Inner::new());
4577 let slot: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
4578 spawn_worker(&slot, &inner, "panicky", || panic!("boom")).unwrap();
4579 slot.lock().unwrap().take().unwrap().join().unwrap();
4580 assert_eq!(inner.state_name(), "failed");
4581 assert_eq!(
4582 inner.last_error().as_deref(),
4583 Some("worker thread panicked: boom")
4584 );
4585 assert!(!inner.worker_alive());
4586 assert_eq!(inner.capture_tid.load(Ordering::Acquire), 0);
4587 }
4588
4589 #[test]
4597 fn failed_start_cleanup_spares_concurrent_winner() {
4598 let inner = Arc::new(Inner::new());
4599 let slot: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
4600
4601 let li = inner.clone();
4602 let l_id = spawn_worker(&slot, &inner, "loser", move || {
4603 li.started_ok.store(false, Ordering::Release);
4604 li.start_state.store(ST_FAILED, Ordering::Release);
4605 })
4606 .unwrap();
4607 while inner.start_state.load(Ordering::Acquire) != ST_FAILED {
4608 std::thread::yield_now();
4609 }
4610
4611 let wi = inner.clone();
4612 let w_id = spawn_worker(&slot, &inner, "winner", move || {
4613 wi.started_ok.store(true, Ordering::Release);
4614 wi.start_state.store(ST_RUNNING, Ordering::Release);
4615 while !wi.stop_pending() {
4616 std::thread::sleep(Duration::from_millis(1));
4617 }
4618 wi.started_ok.store(false, Ordering::Release);
4619 })
4620 .unwrap();
4621 assert_ne!(l_id, w_id);
4622 while inner.start_state.load(Ordering::Acquire) != ST_RUNNING {
4623 std::thread::yield_now();
4624 }
4625
4626 join_failed_start(&slot, &inner, l_id);
4627 assert!(
4628 slot.lock().unwrap().is_some(),
4629 "winner's handle must survive"
4630 );
4631 assert_eq!(inner.stop_state.load(Ordering::Acquire), STOP_NONE);
4632 assert!(inner.worker_alive(), "winner must still be running");
4633
4634 {
4635 let mut g = slot.lock().unwrap();
4636 let h = g.take().expect("winner handle present");
4637 inner.request_external_stop();
4638 h.join().unwrap();
4639 }
4640 assert!(!inner.worker_alive());
4641 }
4642
4643 #[test]
4651 fn concurrent_start_loser_never_kills_winner() {
4652 for _ in 0..200 {
4653 let inner = Arc::new(Inner::new());
4654 let slot = Arc::new(Mutex::new(None::<JoinHandle<()>>));
4655 let runs = Arc::new(AtomicUsize::new(0));
4656
4657 let starter = |name: &'static str| {
4658 let inner = inner.clone();
4659 let slot = slot.clone();
4660 let runs = runs.clone();
4661 std::thread::spawn(move || {
4662 let bi = inner.clone();
4663 let bruns = runs.clone();
4664 let id = spawn_worker(&slot, &inner, name, move || {
4665 if bruns.fetch_add(1, Ordering::AcqRel) == 0 {
4666 bi.started_ok.store(false, Ordering::Release);
4667 bi.start_state.store(ST_FAILED, Ordering::Release);
4668 } else {
4669 bi.started_ok.store(true, Ordering::Release);
4670 bi.start_state.store(ST_RUNNING, Ordering::Release);
4671 let mut spins: u64 = 0;
4672 while !bi.stop_pending() {
4673 std::thread::sleep(Duration::from_millis(1));
4674 spins += 1;
4675 assert!(spins < 10_000, "external stop was lost");
4676 }
4677 bi.started_ok.store(false, Ordering::Release);
4678 }
4679 })
4680 .unwrap();
4681 let mut state = inner.start_state.load(Ordering::Acquire);
4682 let mut tries = 0;
4683 while state == ST_STARTING && tries < 2000 {
4684 std::thread::sleep(Duration::from_millis(1));
4685 state = inner.start_state.load(Ordering::Acquire);
4686 tries += 1;
4687 }
4688 if state == ST_FAILED {
4689 join_failed_start(&slot, &inner, id);
4690 }
4691 })
4692 };
4693 let a = starter("start-a");
4694 let b = starter("start-b");
4695 a.join().unwrap();
4696 b.join().unwrap();
4697
4698 assert_eq!(runs.load(Ordering::Acquire), 2);
4699 assert!(
4700 slot.lock().unwrap().is_some(),
4701 "winner's thread must survive"
4702 );
4703 assert_eq!(inner.start_state.load(Ordering::Acquire), ST_RUNNING);
4704 assert!(
4705 inner.worker_alive(),
4706 "winner must still be alive after both starts"
4707 );
4708
4709 {
4710 let mut g = slot.lock().unwrap();
4711 let h = g.take().expect("winner handle present");
4712 inner.request_external_stop();
4713 h.join().unwrap();
4714 }
4715 assert!(!inner.worker_alive());
4716 }
4717 }
4718
4719 #[test]
4724 fn prefix_writer_matches_build_ws_body() {
4725 let primary: Vec<u8> = (0u8..200).collect();
4726 let mut mixed: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
4727 mixed.push_back((vec![1u8; 100], 0));
4728 mixed.push_back((vec![2u8; RED_MAX_LEN + 1], 960));
4729 mixed.push_back((vec![3u8; 50], 1440));
4730 mixed.push_back((vec![4u8; 900], 1900));
4731 for hist in [VecDeque::new(), mixed] {
4732 for red in [0usize, 1, 2, 4] {
4733 for hdr in [true, false] {
4734 for pts in [960u64, 3840, 20000] {
4735 let reference = build_ws_body(&primary, pts, &hist, red, hdr);
4736 let mut buf = vec![0u8; RED_PREFIX_MAX + MAX_OPUS_PACKET];
4737 let prefix = write_ws_prefix_into(&mut buf, pts, &hist, red, hdr);
4738 buf[prefix..prefix + primary.len()].copy_from_slice(&primary);
4739 buf.truncate(prefix + primary.len());
4740 assert_eq!(buf, reference, "red={red} hdr={hdr} pts={pts}");
4741 }
4742 }
4743 }
4744 }
4745 }
4746
4747 #[test]
4751 fn buffer_pool_recycles_and_restores_length() {
4752 let pool = BufferPool::new(64);
4753 let mut a = pool.take();
4754 assert_eq!(a.len(), 64);
4755 let ptr = a.as_ptr() as usize;
4756 a.truncate(7);
4757 pool.put(a);
4758 let b = pool.take();
4759 assert_eq!(b.as_ptr() as usize, ptr, "buffer must be recycled");
4760 assert_eq!(b.len(), 64, "length must be restored");
4761 pool.put(Vec::new());
4762 assert_eq!(pool.take().len(), 64);
4763 }
4764
4765 #[test]
4768 fn audio_frame_drop_refills_pool() {
4769 let pool = Arc::new(BufferPool::new(32));
4770 let buf = pool.take();
4771 let ptr = buf.as_ptr() as usize;
4772 drop(AudioFrame {
4773 data: buf,
4774 pts: 0,
4775 channels: 2,
4776 pool: Some(Arc::clone(&pool)),
4777 });
4778 let recycled = pool.take();
4779 assert_eq!(recycled.as_ptr() as usize, ptr);
4780 }
4781
4782 #[test]
4791 #[ignore]
4792 fn bench_emit_assembly() {
4793 use std::hint::black_box;
4794 use std::time::Instant;
4795
4796 const ITERS: u32 = 500_000;
4797 const PAYLOAD: usize = 200;
4798 let src = vec![0xA5u8; PAYLOAD];
4799 let mut hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
4800 hist.push_back((vec![6u8; 180], 0));
4801 hist.push_back((vec![7u8; 180], 480));
4802
4803 for (red, label) in [(0usize, "red=0"), (2usize, "red=2")] {
4804 let old = Instant::now();
4805 let mut out = vec![0u8; MAX_OPUS_PACKET];
4806 for i in 0..ITERS {
4807 out[..PAYLOAD].copy_from_slice(&src);
4808 let data = build_ws_body(&out[..PAYLOAD], 960 + i as u64, &hist, red, true);
4809 black_box(&data);
4810 }
4811 let old_ns = old.elapsed().as_nanos() / ITERS as u128;
4812
4813 let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4814 let mut taker = PoolTaker::new(Arc::clone(&pool));
4815 let mut inflight: VecDeque<Vec<u8>> = VecDeque::new();
4816 let new = Instant::now();
4817 for i in 0..ITERS {
4818 let mut data = taker.take();
4819 let prefix = write_ws_prefix_into(&mut data, 960 + i as u64, &hist, red, true);
4820 data[prefix..prefix + PAYLOAD].copy_from_slice(&src);
4821 data.truncate(prefix + PAYLOAD);
4822 black_box(&data);
4823 inflight.push_back(data);
4824 if inflight.len() > 8 {
4825 pool.put(inflight.pop_front().unwrap());
4826 }
4827 }
4828 let new_ns = new.elapsed().as_nanos() / ITERS as u128;
4829 println!("{label}: old {old_ns} ns/frame -> pooled in-place {new_ns} ns/frame");
4830 }
4831 }
4832}
4833
4834#[pymodule]
4840fn pcmflux(m: &Bound<'_, PyModule>) -> PyResult<()> {
4841 m.add_class::<AudioCapture>()?;
4842 m.add_class::<AudioCaptureSettings>()?;
4843 m.add_class::<AudioFrame>()?;
4844 m.add_class::<AudioPlayback>()?;
4845 m.add_class::<AudioPlaybackSettings>()?;
4846 m.add_function(wrap_pyfunction!(_stop_all_captures, m)?)?;
4847 if let Ok(atexit) = m.py().import("atexit") {
4848 let _ = atexit.call_method1("register", (m.getattr("_stop_all_captures")?,));
4849 }
4850 Ok(())
4851}