Skip to main content

mittens_engine/engine/ecs/system/
audio_sample_format_convert.rs

1//! Sample-rate conversion + channel remix.
2//!
3//! Decode is upstream ([`super::audio_decode`]); loudness normalization is
4//! downstream (gain-policy at the playback graph). This stage is
5//! deterministic and data-format-driven only — no gain changes.
6
7use super::audio_decode::DecodedAudio;
8
9#[derive(Debug, Clone, Copy)]
10pub struct PlaybackFormat {
11    pub sample_rate: u32,
12    pub channels: u16,
13}
14
15/// PCM in the engine's playback format. Interleaved frame-major.
16#[derive(Debug, Clone)]
17pub struct ConvertedAudio {
18    pub samples: std::sync::Arc<Vec<f32>>,
19    pub channels: u16,
20    pub sample_rate: u32,
21}
22
23#[derive(Debug, Clone)]
24pub enum ConvertError {
25    InvalidTarget(String),
26    EmptyInput,
27}
28
29impl std::fmt::Display for ConvertError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            ConvertError::InvalidTarget(s) => write!(f, "invalid target: {s}"),
33            ConvertError::EmptyInput => write!(f, "empty input"),
34        }
35    }
36}
37
38impl std::error::Error for ConvertError {}
39
40/// Convert `decoded` into the engine's playback format. Channel remix is
41/// applied before resample so the resampler sees the right channel count.
42pub fn convert_sample_format(
43    decoded: DecodedAudio,
44    target: PlaybackFormat,
45) -> Result<ConvertedAudio, ConvertError> {
46    if target.sample_rate == 0 || target.channels == 0 {
47        return Err(ConvertError::InvalidTarget(format!("{target:?}")));
48    }
49    if decoded.samples.is_empty() {
50        return Err(ConvertError::EmptyInput);
51    }
52
53    let after_channels = remix_channels(&decoded.samples, decoded.channels, target.channels);
54    let after_resample = if decoded.sample_rate == target.sample_rate {
55        after_channels
56    } else {
57        resample_linear(
58            &after_channels,
59            target.channels,
60            decoded.sample_rate,
61            target.sample_rate,
62        )
63    };
64
65    Ok(ConvertedAudio {
66        samples: std::sync::Arc::new(after_resample),
67        channels: target.channels,
68        sample_rate: target.sample_rate,
69    })
70}
71
72fn remix_channels(samples: &[f32], src: u16, dst: u16) -> Vec<f32> {
73    if src == dst {
74        return samples.to_vec();
75    }
76    let src_n = src as usize;
77    let dst_n = dst as usize;
78    let frames = samples.len() / src_n.max(1);
79
80    if dst == 1 {
81        // Downmix to mono via per-frame average.
82        let mut out = Vec::with_capacity(frames);
83        for f in 0..frames {
84            let base = f * src_n;
85            let mut sum = 0.0f32;
86            for c in 0..src_n {
87                sum += samples[base + c];
88            }
89            out.push(sum / src_n as f32);
90        }
91        out
92    } else if src == 1 {
93        // Upmix mono → multi by duplicating into every output channel.
94        let mut out = Vec::with_capacity(frames * dst_n);
95        for f in 0..frames {
96            let s = samples[f];
97            for _ in 0..dst_n {
98                out.push(s);
99            }
100        }
101        out
102    } else {
103        // General N→M: pick first `min(src, dst)` channels, zero-fill the
104        // rest. Good-enough placeholder until a proper down/upmix matrix is
105        // needed.
106        let copy_n = src_n.min(dst_n);
107        let mut out = Vec::with_capacity(frames * dst_n);
108        for f in 0..frames {
109            let base = f * src_n;
110            for c in 0..copy_n {
111                out.push(samples[base + c]);
112            }
113            for _ in copy_n..dst_n {
114                out.push(0.0);
115            }
116        }
117        out
118    }
119}
120
121/// Linear resampler — interleaved PCM in, interleaved PCM out. Good enough
122/// for phase 5; an upgrade to a sinc resampler can swap in later behind
123/// the same signature.
124fn resample_linear(samples: &[f32], channels: u16, src_sr: u32, dst_sr: u32) -> Vec<f32> {
125    if src_sr == dst_sr {
126        return samples.to_vec();
127    }
128    let ch = channels as usize;
129    let in_frames = samples.len() / ch.max(1);
130    if in_frames < 2 {
131        return samples.to_vec();
132    }
133
134    let ratio = dst_sr as f64 / src_sr as f64;
135    let out_frames = ((in_frames as f64) * ratio).round() as usize;
136    let mut out = Vec::with_capacity(out_frames * ch);
137
138    for f in 0..out_frames {
139        let src_pos = f as f64 / ratio;
140        let i0 = src_pos.floor() as usize;
141        let i1 = (i0 + 1).min(in_frames - 1);
142        let t = (src_pos - i0 as f64) as f32;
143        for c in 0..ch {
144            let s0 = samples[i0 * ch + c];
145            let s1 = samples[i1 * ch + c];
146            out.push(s0 + (s1 - s0) * t);
147        }
148    }
149    out
150}