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 capabilities;
34pub use capabilities::{
35    Availability, BackendKind, Capabilities, DeviceSelectionBehavior, DeviceWatcherCapabilities,
36    OsVersion, PidScope, ProcessCapabilities, SideEffectCapabilities, SideEffectCapability,
37    SideEffectCondition, SourceCapabilities, SourceCapability, SystemExclusionCapabilities,
38    UnavailableReason, WatchScope, WatcherMode,
39};
40pub mod chunk_ring;
41pub mod clock;
42pub mod diagnostics;
43pub mod error_context;
44mod errors;
45mod spawn_error;
46pub use spawn_error::spawn_error;
47pub mod inventory;
48mod loss;
49pub mod normalizer;
50pub mod process_list;
51pub mod quant;
52pub mod raw_ring;
53pub mod secondary_ring;
54pub mod startup;
55pub mod types;
56mod vad_error;
57
58// Re-export the main types at the crate root.
59pub use backend::{CaptureBackend, RawSink};
60pub use chunk_ring::{chunk_ring, ChunkConsumer, ChunkProducer};
61pub use clock::{monotonic_now_ns, ClockNormalizer};
62pub use normalizer::{InnerProcessor, NormalizedChunk, Normalizer, CHUNK_FRAMES};
63pub use quant::quantize_i16;
64pub use raw_ring::{raw_ring, RawConsumer, RawProducer};
65pub use secondary_ring::{secondary_chunk_ring, SecondaryChunkConsumer, SecondaryChunkProducer};
66pub use types::{
67    AudioChunk, AudioLoss, AudioPath, ChunkFlags, DefaultDeviceKind, DeviceEvent, DeviceInfo,
68    Error, ErrorContext, ErrorGroup, ErrorKind, Event, LossReason, MixLane, NativeStatus,
69    Operation, OutputFormat, OutputTap, Permission, ProcessInfo, Result, SecondaryChunk,
70    ShutdownReport, SourceKind, StreamConfig, CHANNELS, SAMPLE_RATE,
71};
72
73pub use diagnostics::CaptureDiagnostics;
74pub use startup::StartupDropReason;
75pub use vad_error::VadErrorCode;
76
77/// Public spelling of the canonical chunk-duration option.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ChunkMsOption {
80    /// Rust, Python and C use `chunk_ms`.
81    SnakeCase,
82    /// JavaScript uses `chunkMs`.
83    JavaScript,
84}
85/// One cause/remedy message with the binding's public option name.
86pub fn chunk_ms_error(option: ChunkMsOption) -> Error {
87    let name = match option {
88        ChunkMsOption::SnakeCase => "chunk_ms",
89        ChunkMsOption::JavaScript => "chunkMs",
90    };
91    Error::InvalidArg(format!(
92        "capture uses the canonical 20 ms frame contract; set {name} to 20"
93    ))
94}
95/// Validate the canonical frame duration before acquiring capture resources.
96pub fn validate_chunk_ms(chunk_ms: u32) -> Result<()> {
97    if chunk_ms == 20 {
98        Ok(())
99    } else {
100        Err(chunk_ms_error(ChunkMsOption::SnakeCase))
101    }
102}
103#[cfg(test)]
104mod chunk_contract_tests {
105    #[test]
106    fn chunk_duration_rejection_explains_contract_and_remedy() {
107        let error = super::validate_chunk_ms(10).expect_err("noncanonical duration must fail");
108        let message = error.to_string();
109        assert!(
110            message.contains("canonical 20 ms frame contract"),
111            "{message}"
112        );
113        assert!(message.contains("set chunk_ms to 20"), "{message}");
114        assert!(super::validate_chunk_ms(20).is_ok());
115    }
116}
117
118#[cfg(test)]
119mod chunk_option_tests {
120    #[test]
121    fn javascript_chunk_duration_uses_its_public_option_spelling() {
122        let error = super::chunk_ms_error(super::ChunkMsOption::JavaScript);
123        let message = error.to_string();
124        assert!(message.contains("set chunkMs to 20"), "{message}");
125        assert!(message.contains("canonical 20 ms frame contract"));
126    }
127}