Skip to main content

flexaudio_core/
normalizer.rs

1//! Normalize arbitrary device frames (any sample rate/channel count, interleaved f32)
2//! in two stages, then convert them back to the requested output format.
3//!
4//! ```text
5//! Input (any sample rate/channel count)
6//!   │  Stage 1 (internal normalization; fixed)
7//!   │   - Channel mix (→stereo)
8//!   │   - Sample-rate conversion (rubato, →48000)
9//!   ▼
10//! Internal canonical form: f32 / 48000 Hz / stereo / 20ms = 960 frames
11//!   │  Stage 2 (output; per tap)
12//!   │   - Channel conversion (stereo→mono average / mono→stereo duplicate / unchanged)
13//!   │   - Sample-rate conversion (rubato, 48000→output.sample_rate; passthrough if equal)
14//!   ▼
15//! Output: f32 / output.sample_rate / output.channels / fixed 20ms duration
16//!        (48k=960 / 16k=320 / 8k=160 frames)
17//! ```
18//!
19//! With the default output `{48000, 2}`, stage 2 is entirely passthrough (the internal
20//! canonical form is emitted as-is). Stage 1 sample-rate conversion is bypassed when
21//! `in_sample_rate == 48000`; stage 2 conversion is bypassed when `output.sample_rate == 48000`.
22//!
23//! Both rubato resamplers use `FixedAsync::Input` (fixed input chunks). Variable-length output
24//! is collected in an internal accumulator and split at 20ms boundaries. The resampler and
25//! accumulator carry any remainder forward.
26//!
27//! PTS is projected from the supplied timestamp using input-to-output sample offsets.
28//! The facade supplies ingest arrival times as `device_pts`, not device timestamps,
29//! and assigns `seq`.
30
31use rubato::audioadapter_buffers::direct::InterleavedSlice;
32use rubato::{
33    Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, SincInterpolationType,
34    WindowFunction,
35};
36
37use crate::types::{
38    ChunkFlags, Error, ErrorContext, ErrorGroup, Operation, OutputFormat, Result, CHANNELS,
39    SAMPLE_RATE,
40};
41
42/// A library-constructed chunk retaining padding provenance.
43#[derive(Debug, Clone, PartialEq)]
44#[non_exhaustive]
45pub struct NormalizedChunk {
46    /// Interleaved output PCM.
47    pub samples: Vec<f32>,
48    /// Presentation timestamp of the first frame.
49    pub pts_ns: i64,
50    /// Integrity flags; padding alone is not a discontinuity.
51    pub flags: ChunkFlags,
52}
53
54/// Frames per internal canonical-form chunk (20ms @ 48kHz). Stage 1 split boundary.
55pub const CHUNK_FRAMES: usize = 960;
56
57/// Channel count in the internal canonical form (stereo).
58const INNER_CH: usize = CHANNELS as usize; // 2
59
60/// A processor applied to the internal normalized form (48kHz / stereo /
61/// interleaved f32) *before* it is split into the output taps.
62///
63/// Kept behind a trait so `flexaudio-core` stays independent of any concrete
64/// DSP implementation (e.g. noise suppression). The facade injects an
65/// implementation; the core only sees this contract. Because the same
66/// processed samples feed every output tap, a single processor affects both the
67/// primary and secondary outputs.
68///
69/// [`process`](Self::process) runs on each batch of normalized samples in
70/// place. [`flush`](Self::flush) is called once at stop to recover any tail the
71/// processor is holding back (e.g. a fixed delay line); it returns the trailing
72/// 48kHz / stereo interleaved samples (empty if none).
73pub trait InnerProcessor: Send {
74    /// Process one batch of 48kHz / stereo interleaved samples in place. The
75    /// slice length is always a multiple of two (stereo).
76    fn process(&mut self, samples: &mut [f32]);
77    /// Flush and return any buffered tail (48kHz / stereo interleaved). Called
78    /// once when the stream stops. Returns an empty vector when nothing is held.
79    fn flush(&mut self) -> Vec<f32>;
80}
81
82/// Stateful two-stage converter that normalizes input device frames to the internal canonical
83/// form (48k/stereo/960 frames), then converts them to one or more output taps (primary and
84/// optional secondary).
85///
86/// Stage 1 (internal normalization) runs once and feeds the resulting canonical form to stage 2
87/// of both taps. An optional [`InnerProcessor`] is applied once to the canonical form before it
88/// is split for stage 2, so both taps receive the same processed audio.
89///
90/// Accumulate interleaved samples with `push`, then retrieve completed output chunks one at a
91/// time with `pop_chunk` (primary) and `pop_secondary` (secondary).
92pub struct Normalizer {
93    flush_result: Option<Result<()>>,
94    in_sample_rate: u32,
95    in_channels: usize,
96    /// Interleaved input awaiting a complete native frame.
97    pending_input: Vec<f32>,
98    /// Timestamp of the first sample retained in `pending_input`.
99    pending_input_pts: Option<i64>,
100
101    // --- Stage 1 (internal normalization: → 48k/stereo, shared by all output taps) ---
102    /// Passthrough when the input is 48000 Hz (no resampler).
103    stage1_resampler: Option<ResamplerState>,
104    /// Temporary buffer for the internal canonical form (48k/stereo interleaved) generated by
105    /// this push. Apply [`InnerProcessor`] before distributing it to each output tap. Reuse capacity.
106    inner_scratch: Vec<f32>,
107    /// Total internal 48k frames generated by stage 1 so far (for PTS anchor calculation).
108    total_inner_frames: u64,
109
110    /// Optional processor (e.g. noise suppression) applied once to the internal canonical form
111    /// before the stage 2 split.
112    inner_processor: Option<Box<dyn InnerProcessor>>,
113
114    // --- Output taps (each has its own stage 2, output buffer, and PTS state) ---
115    /// Primary output tap (its [`OutputFormat`] is `output`).
116    primary: OutputTap,
117    capture: Option<OutputTap>,
118    /// Secondary output tap (when configured). Has stage 2 and PTS state independent of primary.
119    secondary: Option<OutputTap>,
120}
121
122/// One output tap. Receives the shared internal canonical form (48k/stereo), converts channels
123/// and sample rate to its [`OutputFormat`], then emits fixed 20ms chunks. Each tap has its own
124/// PTS anchor and output buffer (which can cause tens of milliseconds of PTS offset between taps).
125struct OutputTap {
126    output: OutputFormat,
127    /// Output stage. `None` (full passthrough) when `output == {48000, 2}`, matching the canonical form.
128    stage2: Option<OutputStage>,
129    /// Output awaiting completion (interleaved with `output.channels`). `pop` drains from here.
130    out_buf: Vec<f32>,
131    /// Frames in one output chunk (`output.chunk_frames()`).
132    out_chunk_frames: usize,
133    /// Output channel count.
134    out_channels: usize,
135    /// Output frame index for the first sample in `out_buf` (the oldest sample not yet popped).
136    out_frame_origin: u64,
137    /// PTS anchor: associates an output frame index with `device_pts` (ns).
138    pts_anchor: Option<PtsAnchor>,
139    /// Exact integer projection for capture anchors; output taps retain their legacy projection.
140    canonical_clock: bool,
141    padding: std::collections::VecDeque<std::ops::Range<u64>>,
142}
143
144#[derive(Clone, Copy)]
145struct PtsAnchor {
146    /// Output frame index, based on the output sample rate.
147    out_frame: u64,
148    /// `device_pts` (ns) corresponding to that output frame.
149    pts_ns: i64,
150}
151
152/// Sample-rate converter wrapping rubato `Async` (`FixedAsync::Input`) for one stage.
153///
154/// Calls `process` for each fixed input chunk of `chunk_in_frames` frames and appends variable-length
155/// output to `out_buf` (the caller's accumulator). `channels` varies by stage (always stereo=2 in
156/// stage 1; the output channel count in stage 2).
157struct ResamplerState {
158    inner: Async<f32>,
159    channels: usize,
160    /// Input frames required per rubato call (fixed by `FixedAsync::Input`).
161    chunk_in_frames: usize,
162    /// Maximum output frames one `process` call can produce.
163    max_out_frames: usize,
164    /// Unprocessed input (interleaved, `channels` channels).
165    in_accum: Vec<f32>,
166    /// Output scratch buffer for rubato (reused to avoid allocations).
167    out_scratch: Vec<f32>,
168}
169
170/// Stage 2 (output). Receives 960-frame chunks in the internal 48k/stereo canonical form and
171/// converts channels and sample rate to produce interleaved output in the requested format.
172struct OutputStage {
173    out_channels: usize,
174    /// Resampler from 48000 to `output.sample_rate`. `None` (sample-rate passthrough) when
175    /// `output.sample_rate == 48000`. Applied to samples after channel conversion.
176    resampler: Option<ResamplerState>,
177    /// Scratch buffer after channel conversion and before sample-rate conversion (48k / `out_channels` interleaved).
178    ch_scratch: Vec<f32>,
179}
180
181impl Normalizer {
182    /// Create a normalizer for the input sample rate, input channel count, and output format.
183    ///
184    /// Stage 1 normalizes input to 48k/stereo (`in_sample_rate == 48000` bypasses sample-rate
185    /// conversion; `in_channels` of 1 duplicates mono to stereo and 2 is unchanged.
186    /// Input above two channels is rejected with UnsupportedFormat). Stage 2 converts the canonical form to `output` (passthrough
187    /// when `output == {48000, 2}`).
188    ///
189    /// Expects the caller to have run [`OutputFormat::validate`] on `output` (this method does not
190    /// clamp it to a valid range).
191    ///
192    /// rubato resampler construction can fail, for example with extreme rate ratios. Panicking
193    /// would silently stop the non-RT capture thread, so failures return [`Error::Backend`] and
194    /// propagate to the caller.
195    pub fn new(in_sample_rate: u32, in_channels: u16, output: OutputFormat) -> Result<Self> {
196        if in_sample_rate == 0 || in_channels == 0 {
197            return Err(Error::InvalidArg(
198                "native rate and channels must be positive".into(),
199            ));
200        }
201        if in_channels > 2 {
202            return Err(Error::UnsupportedFormat(
203                "native input supports only mono or stereo".into(),
204            ));
205        }
206        output.validate()?;
207        let in_channels = usize::from(in_channels);
208
209        // Stage 1 resampler (→48000). Runs once for all output taps.
210        let stage1_resampler = if in_sample_rate == SAMPLE_RATE {
211            None
212        } else {
213            Some(ResamplerState::new(in_sample_rate, SAMPLE_RATE, INNER_CH)?)
214        };
215
216        Ok(Self {
217            flush_result: None,
218            in_sample_rate,
219            in_channels,
220            pending_input: Vec::new(),
221            pending_input_pts: None,
222            stage1_resampler,
223            inner_scratch: Vec::with_capacity(CHUNK_FRAMES * INNER_CH * 4),
224            total_inner_frames: 0,
225            inner_processor: None,
226            primary: OutputTap::new(output)?,
227            capture: None,
228            secondary: None,
229        })
230    }
231
232    /// Add a secondary output tap (expects [`OutputFormat::validate`] to have been called).
233    ///
234    /// The internal canonical form (48k/stereo) is generated once and fed to stage 2 of both taps.
235    /// The secondary tap has independent stage 2 and PTS state, and emits its own fixed 20ms chunks.
236    /// rubato construction failures return [`Error::Backend`].
237    pub fn with_secondary(mut self, secondary: OutputFormat) -> Result<Self> {
238        self.secondary = Some(OutputTap::new(secondary)?);
239        Ok(self)
240    }
241
242    /// Enable a pre-output-conversion canonical branch for off-callback consumers.
243    /// Its stop tail contains valid frames only, never transport padding.
244    pub fn with_capture_tap(mut self) -> Result<Self> {
245        let mut capture = OutputTap::new(OutputFormat::default())?;
246        capture.canonical_clock = true;
247        self.capture = Some(capture);
248        Ok(self)
249    }
250
251    /// Canonical frames produced but not emitted, discarded when this generation is rebuilt.
252    pub fn buffered_capture_frames(&self) -> usize {
253        self.capture
254            .as_ref()
255            .map_or(0, OutputTap::buffered_out_frames)
256    }
257
258    /// Retrieve valid canonical stereo frames; allow a short final tail only at stop.
259    pub fn pop_capture(&mut self, stopping: bool) -> Option<(Vec<f32>, i64)> {
260        self.pop_capture_with_metadata(stopping)
261            .map(|chunk| (chunk.samples, chunk.pts_ns))
262    }
263
264    /// Retrieve valid canonical frames with no transport padding.
265    pub fn pop_capture_with_metadata(&mut self, stopping: bool) -> Option<NormalizedChunk> {
266        let tap = self.capture.as_mut()?;
267        let frames = tap.buffered_out_frames();
268        if frames == 0 || (frames < CHUNK_FRAMES && !stopping) {
269            return None;
270        }
271        let take = frames.min(CHUNK_FRAMES);
272        let pts = tap.pts_for_out_frame(tap.out_frame_origin);
273        let data = tap.out_buf.drain(..take * INNER_CH).collect();
274        tap.out_frame_origin += take as u64;
275        Some(NormalizedChunk {
276            samples: data,
277            pts_ns: pts,
278            flags: ChunkFlags::empty(),
279        })
280    }
281
282    /// Inject a processor to apply once to the internal canonical form before the stage 2 split.
283    pub fn with_inner_processor(mut self, processor: Box<dyn InnerProcessor>) -> Self {
284        self.inner_processor = Some(processor);
285        self
286    }
287
288    /// Input sample rate (Hz).
289    pub fn in_sample_rate(&self) -> u32 {
290        self.in_sample_rate
291    }
292
293    /// Primary output format.
294    pub fn output(&self) -> OutputFormat {
295        self.primary.output
296    }
297
298    /// Secondary output format (only when a secondary tap is configured).
299    pub fn secondary_output(&self) -> Option<OutputFormat> {
300        self.secondary.as_ref().map(|t| t.output)
301    }
302
303    /// Whether a secondary tap is enabled.
304    pub fn has_secondary(&self) -> bool {
305        self.secondary.is_some()
306    }
307
308    /// Whether stage 1 sample-rate conversion is bypassed (`in == 48000`).
309    pub fn is_passthrough(&self) -> bool {
310        self.stage1_resampler.is_none()
311    }
312
313    /// Whether stage 2 for the primary output is fully bypassed (`output == {48000, 2}`).
314    pub fn is_output_passthrough(&self) -> bool {
315        self.primary.stage2.is_none()
316    }
317
318    /// Accumulate interleaved input samples.
319    ///
320    /// Incomplete trailing frames are retained until a later push completes them.
321    /// `device_pts_ns` labels the first frame in this push; the facade supplies an ingest
322    /// arrival time, not a device timestamp.
323    ///
324    /// Returns [`Error::Backend`] if rubato's `process` fails. This avoids silently stopping the
325    /// capture thread with a panic and lets the caller stop the stream explicitly.
326    pub fn push(&mut self, interleaved: &[f32], device_pts_ns: i64) -> Result<()> {
327        if interleaved.is_empty() {
328            return Ok(());
329        }
330        if let Some(Err(error)) = &self.flush_result {
331            return Err(error.clone());
332        }
333        self.flush_result = None;
334        if self.pending_input.is_empty() {
335            self.pending_input_pts = Some(device_pts_ns);
336        }
337        self.pending_input.extend_from_slice(interleaved);
338        let in_frames = self.pending_input.len() / self.in_channels;
339        if in_frames == 0 {
340            return Ok(());
341        }
342        let device_pts_ns = self.pending_input_pts.unwrap_or(device_pts_ns);
343
344        // Approximate the future output frame position for the start of this push by the sample-rate
345        // ratio and update each tap's PTS anchor. This is approximate because the resampler retains
346        // a remainder internally. Set anchors independently for primary and secondary.
347        self.primary
348            .update_pts_anchor(self.total_inner_frames, device_pts_ns)?;
349        if let Some(sec) = self.secondary.as_mut() {
350            sec.update_pts_anchor(self.total_inner_frames, device_pts_ns)?;
351        }
352
353        if let Some(capture) = self.capture.as_mut() {
354            // One rational PTS anchor per source generation. Arrival-time jitter must
355            // not reanchor old samples or create a VAD epoch on every push.
356            if capture.pts_anchor.is_none() {
357                capture.update_pts_anchor(self.total_inner_frames, device_pts_ns)?;
358            }
359        }
360
361        // Stage 1: mix channels → stereo interleaved → normalize to 48k. Collect what this push
362        // generates in inner_scratch.
363        self.inner_scratch.clear();
364        let mut stereo = Vec::with_capacity(in_frames * INNER_CH);
365        Self::mix_to_stereo(
366            &self.pending_input,
367            self.in_channels,
368            in_frames,
369            &mut stereo,
370        );
371        self.pending_input.drain(..in_frames * self.in_channels);
372        self.pending_input_pts = if self.pending_input.is_empty() {
373            None
374        } else {
375            Some(device_pts_ns.saturating_add(
376                (in_frames as i64).saturating_mul(1_000_000_000) / self.in_sample_rate as i64,
377            ))
378        };
379        match &mut self.stage1_resampler {
380            None => {
381                // Sample-rate passthrough. Use directly as the internal canonical form.
382                self.total_inner_frames += in_frames as u64;
383                self.inner_scratch.extend_from_slice(&stereo);
384            }
385            Some(rs) => {
386                rs.in_accum.extend_from_slice(&stereo);
387                let produced = rs.drain_into(&mut self.inner_scratch)?;
388                self.total_inner_frames += produced;
389            }
390        }
391
392        // Apply a processor (e.g. denoise) once to the internal canonical form before the stage 2 split.
393        if let Some(proc) = self.inner_processor.as_mut() {
394            proc.process(&mut self.inner_scratch);
395        }
396
397        // Distribute the processed canonical form to stage 2 of both taps.
398        self.distribute_inner()
399    }
400
401    /// Retrieve one completed primary output chunk.
402    ///
403    /// Returns (`out_chunk_frames` interleaved frames at `output.channels`, PTS in ns for the
404    /// first sample). Returns `None` if a full chunk is not buffered.
405    pub fn pop_chunk(&mut self) -> Option<(Vec<f32>, i64)> {
406        self.pop_chunk_with_metadata()
407            .map(|chunk| (chunk.samples, chunk.pts_ns))
408    }
409
410    /// Retrieve one completed secondary output chunk (`None` if no secondary tap is configured).
411    pub fn pop_secondary(&mut self) -> Option<(Vec<f32>, i64)> {
412        self.pop_secondary_with_metadata()
413            .map(|chunk| (chunk.samples, chunk.pts_ns))
414    }
415
416    /// Retrieve primary PCM with its integrity flags.
417    pub fn pop_chunk_with_metadata(&mut self) -> Option<NormalizedChunk> {
418        self.primary.pop()
419    }
420
421    /// Retrieve secondary PCM with its integrity flags.
422    pub fn pop_secondary_with_metadata(&mut self) -> Option<NormalizedChunk> {
423        self.secondary.as_mut().and_then(OutputTap::pop)
424    }
425
426    /// Drain stage 1, processor tail and all output stages, retaining independent failures.
427    /// Repeated flush returns the same result without generating duplicate PCM.
428    /// Valid output remains drainable on cleanup-only failure.
429    pub fn flush(&mut self) -> Result<()> {
430        if let Some(result) = &self.flush_result {
431            return result.clone();
432        }
433        let mut errors = Vec::new();
434        if !self.pending_input.is_empty() {
435            errors.push(Error::Backend("incomplete native frame at flush".into()));
436            self.pending_input.clear();
437            self.pending_input_pts = None;
438        }
439        self.inner_scratch.clear();
440        if let Some(stage) = self.stage1_resampler.as_mut() {
441            match stage.flush_into(&mut self.inner_scratch) {
442                Ok(produced) => self.total_inner_frames += produced,
443                Err(error) => errors.push(error),
444            }
445            if let Some(processor) = self.inner_processor.as_mut() {
446                processor.process(&mut self.inner_scratch);
447            }
448            if let Err(error) = self.distribute_inner() {
449                errors.push(error);
450            }
451        }
452        if let Some(processor) = self.inner_processor.as_mut() {
453            self.inner_scratch = processor.flush();
454            // Keep the canonical capture epoch empty for a primed silent processor.
455            let empty_capture = if self.total_inner_frames == 0 {
456                self.capture.take()
457            } else {
458                None
459            };
460            if let Err(error) = self.distribute_inner() {
461                errors.push(error);
462            }
463            if let Some(capture) = empty_capture {
464                self.capture = Some(capture);
465            }
466        }
467        if let Err(error) = self.primary.flush() {
468            errors.push(error);
469        }
470        if let Some(secondary) = self.secondary.as_mut() {
471            if let Err(error) = secondary.flush() {
472                errors.push(error);
473            }
474        }
475        if let Some(capture) = self.capture.as_mut() {
476            // Preserve canonical valid-frame tails without transport padding.
477            if let Some(stage) = capture.stage2.as_mut() {
478                if let Err(error) = stage.flush_into(&mut capture.out_buf) {
479                    errors.push(error);
480                }
481            }
482        }
483        let result = combine_failures(errors)
484            .map_err(|error| error.with_context(ErrorContext::new(Operation::Flush)));
485        self.flush_result = Some(result.clone());
486        result
487    }
488
489    /// Number of output frames buffered but not yet retrieved from `out_buf` (primary tap).
490    pub fn buffered_out_frames(&self) -> usize {
491        self.primary.buffered_out_frames()
492    }
493
494    // --- Internal helpers ---
495
496    /// Distribute the canonical form in `inner_scratch` to stage 2 of both taps. Temporarily take
497    /// the buffer to avoid borrow conflicts, then restore its capacity.
498    fn distribute_inner(&mut self) -> Result<()> {
499        if self.inner_scratch.is_empty() {
500            return Ok(());
501        }
502        let inner = std::mem::take(&mut self.inner_scratch);
503        let r_primary = self.primary.feed_inner(&inner);
504        let r_capture = self
505            .capture
506            .as_mut()
507            .map(|capture| capture.feed_inner(&inner))
508            .unwrap_or(Ok(()));
509        let r_secondary = self
510            .secondary
511            .as_mut()
512            .map(|sec| sec.feed_inner(&inner))
513            .unwrap_or(Ok(()));
514        // Restore the buffer so its capacity can be reused.
515        self.inner_scratch = inner;
516        self.inner_scratch.clear();
517        combine_failures(
518            [r_primary, r_capture, r_secondary]
519                .into_iter()
520                .filter_map(Result::err)
521                .collect(),
522        )
523    }
524
525    /// Convert validated mono/stereo input to stereo interleaved and push it to `dst`.
526    fn mix_to_stereo(src: &[f32], in_ch: usize, in_frames: usize, dst: &mut Vec<f32>) {
527        match in_ch {
528            1 => {
529                // mono → stereo (duplicate L=R)
530                for &s in &src[..in_frames] {
531                    dst.push(s);
532                    dst.push(s);
533                }
534            }
535            2 => {
536                // Keep 2 channels unchanged (only the required samples).
537                dst.extend_from_slice(&src[..in_frames * 2]);
538            }
539            _ => unreachable!("native channel count validated by constructor"),
540        }
541    }
542}
543
544#[cfg(test)]
545mod tests;
546
547#[cfg(test)]
548mod repro_tests;
549
550mod output;
551mod resampler;
552
553fn combine_failures(errors: Vec<Error>) -> Result<()> {
554    let mut errors = errors.into_iter();
555    match (errors.next(), errors.next()) {
556        (None, _) => Ok(()),
557        (Some(error), None) => Err(error),
558        (Some(primary), Some(first)) => Err(Error::Multiple(ErrorGroup::new(
559            primary,
560            first,
561            errors.collect(),
562        ))),
563    }
564}