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 capture clock; legacy output taps retain their original arithmetic.
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}