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