Skip to main content

flexaudio_core/
lib.rs

1//! flexaudio-core — OS-independent core.
2//!
3//! The OS-independent part of the desktop audio capture abstraction library `flexaudio`.
4//! Provides ring buffers, sample-rate conversion, channel mixing, 20 ms chunking, clock normalization,
5//! events, and type definitions. OS-specific capture is implemented by separate `flexaudio-os-*`
6//! crates that implement [`backend::CaptureBackend`], which the facade layer wires to the core.
7//!
8//! # Fixed contract
9//! All internal processing uses interleaved `f32` / 48000 Hz / stereo 2ch / 20 ms = 960
10//! frames/chunk. [`OutputFormat`] can change the external rate/channels (the Normalizer's second
11//! stage resamples; e.g. 16k/1ch gives 320 frames/chunk). Output chunks remain 20 ms regardless of rate.
12//!
13//! The public API has no callbacks. RT threads only push; consumers poll.
14//! The RT path never blocks (DROP_OLDEST / overflow drop when full). PTS comes from the device
15//! and gaps are detected.
16//!
17//! # Two-stage ring-buffer layout
18//! ```text
19//! [RT cb] --push--> RawRing (rtrb, RT-safe) --pop--> [ingest/process thread]
20//!                                                       |
21//!                                          Normalizer (mix + rubato SRC + 960-frame slicing)
22//!                                                       |
23//!                                                       v
24//!                                       ChunkRing (ringbuf, DROP_OLDEST) --try_pop--> [poll]
25//! ```
26
27#![warn(missing_docs)]
28
29mod addon_error;
30pub use addon_error::AddonErrorKind;
31
32pub mod backend;
33pub mod chunk_ring;
34pub mod clock;
35pub mod diagnostics;
36pub mod error_context;
37mod errors;
38mod spawn_error;
39pub use spawn_error::spawn_error;
40pub mod inventory;
41mod loss;
42pub mod normalizer;
43pub mod process_list;
44pub mod quant;
45pub mod raw_ring;
46pub mod secondary_ring;
47pub mod startup;
48pub mod types;
49mod vad_error;
50
51// Re-export the main types at the crate root.
52pub use backend::{CaptureBackend, RawSink};
53pub use chunk_ring::{chunk_ring, ChunkConsumer, ChunkProducer};
54pub use clock::{monotonic_now_ns, ClockNormalizer};
55pub use normalizer::{InnerProcessor, NormalizedChunk, Normalizer, CHUNK_FRAMES};
56pub use quant::quantize_i16;
57pub use raw_ring::{raw_ring, RawConsumer, RawProducer};
58pub use secondary_ring::{secondary_chunk_ring, SecondaryChunkConsumer, SecondaryChunkProducer};
59pub use types::{
60    AudioChunk, AudioLoss, AudioPath, ChunkFlags, DefaultDeviceKind, DeviceEvent, DeviceInfo,
61    Error, ErrorContext, ErrorGroup, ErrorKind, Event, LossReason, MixLane, NativeStatus,
62    Operation, OutputFormat, OutputTap, Permission, ProcessInfo, Result, SecondaryChunk,
63    ShutdownReport, SourceKind, StreamConfig, CHANNELS, SAMPLE_RATE,
64};
65
66pub use diagnostics::CaptureDiagnostics;
67pub use startup::StartupDropReason;
68pub use vad_error::VadErrorCode;
69
70/// Public spelling of the canonical chunk-duration option.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum ChunkMsOption {
73    /// Rust, Python and C use `chunk_ms`.
74    SnakeCase,
75    /// JavaScript uses `chunkMs`.
76    JavaScript,
77}
78/// One cause/remedy message with the binding's public option name.
79pub fn chunk_ms_error(option: ChunkMsOption) -> Error {
80    let name = match option {
81        ChunkMsOption::SnakeCase => "chunk_ms",
82        ChunkMsOption::JavaScript => "chunkMs",
83    };
84    Error::InvalidArg(format!(
85        "capture uses the canonical 20 ms frame contract; set {name} to 20"
86    ))
87}
88/// Validate the canonical frame duration before acquiring capture resources.
89pub fn validate_chunk_ms(chunk_ms: u32) -> Result<()> {
90    if chunk_ms == 20 {
91        Ok(())
92    } else {
93        Err(chunk_ms_error(ChunkMsOption::SnakeCase))
94    }
95}
96#[cfg(test)]
97mod chunk_contract_tests {
98    #[test]
99    fn chunk_duration_rejection_explains_contract_and_remedy() {
100        let error = super::validate_chunk_ms(10).expect_err("noncanonical duration must fail");
101        let message = error.to_string();
102        assert!(
103            message.contains("canonical 20 ms frame contract"),
104            "{message}"
105        );
106        assert!(message.contains("set chunk_ms to 20"), "{message}");
107        assert!(super::validate_chunk_ms(20).is_ok());
108    }
109}
110
111#[cfg(test)]
112mod chunk_option_tests {
113    #[test]
114    fn javascript_chunk_duration_uses_its_public_option_spelling() {
115        let error = super::chunk_ms_error(super::ChunkMsOption::JavaScript);
116        let message = error.to_string();
117        assert!(message.contains("set chunkMs to 20"), "{message}");
118        assert!(message.contains("canonical 20 ms frame contract"));
119    }
120}