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