Skip to main content

flexaudio_vad/
resample.rs

1//! [`crate::Vad::process_pcm`] の前段。任意フォーマットの interleaved PCM を mono に
2//! 落とし、VAD の動作レート(16000 か 8000)へリサンプルして、既存の 16k/mono 経路へ
3//! 渡せる形にする。
4//!
5//! 流儀は flexaudio-core の正規化器に合わせてある。mono 化は各チャンネルの単純平均
6//! (stereo なら L/R 平均)、リサンプルはアンチエイリアス込みの rubato sinc。リサンプラは
7//! 呼び出しをまたいで内部遅延と端数を持ち越すので、細切れに渡しても継ぎ目は出ない。
8
9use rubato::audioadapter_buffers::direct::InterleavedSlice;
10use rubato::{
11    Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, SincInterpolationType,
12    WindowFunction,
13};
14
15/// [`crate::Vad::process_pcm`] が受け取る入力 PCM のフォーマット記述子。
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct PcmFormat {
18    /// 入力サンプルレート (Hz)。
19    pub sample_rate: u32,
20    /// 入力チャンネル数(interleaved のチャンネル数)。
21    pub channels: u16,
22}
23
24/// interleaved の任意 ch を mono へ落とし、VAD レートへリサンプルする前段変換器。
25///
26/// 入力フォーマット([`PcmFormat`])と目標レートは生成時に固定する。入力レートが目標と
27/// 同じ場合は mono 化だけ行い、リサンプラは持たない。
28pub(crate) struct PcmConverter {
29    format: PcmFormat,
30    channels: usize,
31    /// 目標レートへの SR 変換器。入力レートが目標と一致するなら `None`(mono 化のみ)。
32    resampler: Option<MonoResampler>,
33    /// 1 フレームに満たない端数 interleaved サンプル。次回入力の前に連結して持ち越す。
34    remainder: Vec<f32>,
35    /// mono 化した結果を溜めるスクラッチ(アロケーションの使い回し)。
36    mono: Vec<f32>,
37}
38
39impl PcmConverter {
40    /// 入力フォーマットと目標レート(VAD の動作レート)から変換器を作る。
41    ///
42    /// rubato の構築は極端なレート比などで失敗し得るので、その場合はエラー文字列を返す
43    /// (呼び出し側で panic させずに扱う)。
44    pub(crate) fn new(format: PcmFormat, target_rate: u32) -> Result<Self, String> {
45        let channels = usize::from(format.channels.max(1));
46        let resampler = if format.sample_rate == target_rate {
47            None
48        } else {
49            Some(MonoResampler::new(format.sample_rate, target_rate)?)
50        };
51        Ok(PcmConverter {
52            format,
53            channels,
54            resampler,
55            remainder: Vec::new(),
56            mono: Vec::new(),
57        })
58    }
59
60    /// この変換器が対象とする入力フォーマットか。
61    pub(crate) fn matches(&self, format: PcmFormat) -> bool {
62        self.format == format
63    }
64
65    /// interleaved 入力を mono 化し、必要ならリサンプルして、目標レートの mono サンプルを
66    /// `out` へ追記する。
67    ///
68    /// フレーム境界(channels の倍数)に満たない端数は内部に持ち越すので、任意の位置で
69    /// 分割して渡しても一括で渡したときと同じ結果になる。
70    pub(crate) fn convert(
71        &mut self,
72        interleaved: &[f32],
73        out: &mut Vec<f32>,
74    ) -> Result<(), String> {
75        // 前回の端数に今回分を連結し、揃ったフレームだけ mono 化する。
76        self.remainder.extend_from_slice(interleaved);
77        let frames = self.remainder.len() / self.channels;
78        let used = frames * self.channels;
79
80        self.mono.clear();
81        downmix_to_mono(&self.remainder[..used], self.channels, &mut self.mono);
82        self.remainder.drain(..used);
83
84        match &mut self.resampler {
85            None => out.extend_from_slice(&self.mono),
86            Some(rs) => rs.push(&self.mono, out)?,
87        }
88        Ok(())
89    }
90}
91
92/// interleaved の `channels` ch を各フレームのチャンネル平均で mono 化し `out` へ push する。
93///
94/// stereo は L/R 平均、それ以上は全チャンネルの平均。`channels <= 1` はそのままコピー。
95/// `src` の長さは `channels` の倍数であること(端数フレームは呼び出し側で除いておく)。
96fn downmix_to_mono(src: &[f32], channels: usize, out: &mut Vec<f32>) {
97    if channels <= 1 {
98        out.extend_from_slice(src);
99        return;
100    }
101    let inv = 1.0 / channels as f32;
102    for frame in src.chunks_exact(channels) {
103        let sum: f32 = frame.iter().sum();
104        out.push(sum * inv);
105    }
106}
107
108/// mono 1ch 専用の rubato sinc リサンプラ。固定入力チャンク(`FixedAsync::Input`)で
109/// 動き、端数と内部遅延は呼び出しをまたいで持ち越す。
110///
111/// パラメータは flexaudio-core の正規化器と同じ(sinc_len=128 / BlackmanHarris2 など)。
112struct MonoResampler {
113    inner: Async<f32>,
114    /// rubato が 1 回の `process` で要求する入力フレーム数(固定)。
115    chunk_in_frames: usize,
116    /// 1 回の `process` が生成しうる最大出力フレーム数。
117    max_out_frames: usize,
118    /// 未処理の入力 mono サンプル。
119    in_accum: Vec<f32>,
120    /// rubato への出力スクラッチ(使い回してアロケートを避ける)。
121    out_scratch: Vec<f32>,
122}
123
124impl MonoResampler {
125    fn new(in_sr: u32, out_sr: u32) -> Result<Self, String> {
126        let ratio = out_sr as f64 / in_sr as f64;
127        // 固定入力チャンクは 20ms 相当の入力フレーム(端数は rubato が内部に保持する)。
128        let chunk_in_frames = (in_sr as usize / 50).max(64);
129
130        let params = SincInterpolationParameters {
131            sinc_len: 128,
132            f_cutoff: 0.95,
133            interpolation: SincInterpolationType::Linear,
134            oversampling_factor: 128,
135            window: WindowFunction::BlackmanHarris2,
136        };
137
138        let inner = Async::<f32>::new_sinc(
139            ratio,
140            1.0, // 比は固定
141            &params,
142            chunk_in_frames,
143            1, // mono
144            FixedAsync::Input,
145        )
146        .map_err(|e| format!("rubato sinc resampler construction failed: {e}"))?;
147
148        let max_out_frames = inner.output_frames_max();
149
150        Ok(MonoResampler {
151            inner,
152            chunk_in_frames,
153            max_out_frames,
154            in_accum: Vec::with_capacity(chunk_in_frames * 4),
155            out_scratch: vec![0.0; max_out_frames],
156        })
157    }
158
159    /// mono 入力を溜め、`chunk_in_frames` 単位で可能な限りリサンプルして `out` へ追記する。
160    /// 満たない端数は `in_accum` に残して次回へ持ち越す。
161    fn push(&mut self, mono: &[f32], out: &mut Vec<f32>) -> Result<(), String> {
162        self.in_accum.extend_from_slice(mono);
163        let step = self.chunk_in_frames; // mono なのでフレーム数 = サンプル数。
164
165        while self.in_accum.len() >= step {
166            let in_adapter = InterleavedSlice::new(&self.in_accum[..step], 1, self.chunk_in_frames)
167                .map_err(|e| format!("rubato interleaved input adapter failed: {e}"))?;
168            let mut out_adapter =
169                InterleavedSlice::new_mut(&mut self.out_scratch[..], 1, self.max_out_frames)
170                    .map_err(|e| format!("rubato interleaved output adapter failed: {e}"))?;
171
172            let indexing = Indexing {
173                input_offset: 0,
174                output_offset: 0,
175                partial_len: None,
176                active_channels_mask: None,
177            };
178
179            let (_in_used, out_written) = self
180                .inner
181                .process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
182                .map_err(|e| format!("rubato process_into_buffer failed: {e}"))?;
183
184            out.extend_from_slice(&self.out_scratch[..out_written]); // mono。
185            self.in_accum.drain(..step);
186        }
187        Ok(())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use std::f32::consts::PI;
195
196    #[test]
197    fn downmix_stereo_is_lr_average() {
198        // 完全逆相は 0、同相は元の値。
199        let src = [0.5, -0.5, 0.3, 0.3, 1.0, 0.0];
200        let mut out = Vec::new();
201        downmix_to_mono(&src, 2, &mut out);
202        assert_eq!(out, vec![0.0, 0.3, 0.5]);
203    }
204
205    #[test]
206    fn downmix_quad_is_channel_average() {
207        let src = [1.0, 2.0, 3.0, 4.0]; // 1 フレーム 4ch → 平均 2.5。
208        let mut out = Vec::new();
209        downmix_to_mono(&src, 4, &mut out);
210        assert_eq!(out, vec![2.5]);
211    }
212
213    #[test]
214    fn downmix_mono_is_copy() {
215        let src = [0.1, -0.2, 0.3];
216        let mut out = Vec::new();
217        downmix_to_mono(&src, 1, &mut out);
218        assert_eq!(out, src.to_vec());
219    }
220
221    /// 正弦波を 48k→16k へリサンプルしても周波数(ゼロ交差)と振幅(RMS)が保たれる。
222    /// 過渡を避けて中央だけで測る。
223    #[test]
224    fn resample_48k_to_16k_preserves_tone() {
225        let mut conv = PcmConverter::new(
226            PcmFormat {
227                sample_rate: 48_000,
228                channels: 1,
229            },
230            16_000,
231        )
232        .unwrap();
233
234        let freq = 440.0_f32;
235        let amp = 0.5_f32;
236        let mut out = Vec::new();
237        // 2 秒ぶん、441 サンプルずつ push(細切れでも継ぎ目が出ないことも兼ねる)。
238        let total = 48_000 * 2;
239        let mut i = 0usize;
240        while i < total {
241            let take = 441.min(total - i);
242            let block: Vec<f32> = (0..take)
243                .map(|k| (2.0 * PI * freq * ((i + k) as f32) / 48_000.0).sin() * amp)
244                .collect();
245            conv.convert(&block, &mut out).unwrap();
246            i += take;
247        }
248        assert!(out.len() >= 16_000, "1 秒以上の出力が必要: {}", out.len());
249
250        // 過渡(先頭・末尾各 0.25 秒 = 4000 sample)を捨てて中央 1 秒で測る。
251        let mid = &out[4_000..4_000 + 16_000];
252
253        // 周波数: 16000 sample 中のゼロ交差 ≈ 2*440 = 880。
254        let crossings = zero_crossings(mid);
255        assert!(
256            (876..=884).contains(&crossings),
257            "16k リサンプル後の周波数がずれた: crossings={crossings}"
258        );
259
260        // 振幅: 正弦の RMS は amp/√2 ≈ 0.3536。
261        let got = rms(mid);
262        let expect = amp / std::f32::consts::SQRT_2;
263        assert!(
264            (got - expect).abs() < 0.02,
265            "16k リサンプル後の RMS がずれた: got={got} expect={expect}"
266        );
267    }
268
269    /// SR が目標と一致するときはリサンプラを持たず(mono 化のみ)、mono 化した値が
270    /// そのまま出る。
271    #[test]
272    fn same_rate_stereo_only_downmixes() {
273        let mut conv = PcmConverter::new(
274            PcmFormat {
275                sample_rate: 16_000,
276                channels: 2,
277            },
278            16_000,
279        )
280        .unwrap();
281        assert!(conv.resampler.is_none());
282        let mut out = Vec::new();
283        conv.convert(&[0.5, -0.5, 0.2, 0.2], &mut out).unwrap();
284        assert_eq!(out, vec![0.0, 0.2]);
285    }
286
287    /// フレーム境界(ch の倍数)に満たない端数を挟んで分割しても、一括と同じ mono 列。
288    #[test]
289    fn split_across_partial_frame_matches_bulk() {
290        let fmt = PcmFormat {
291            sample_rate: 16_000,
292            channels: 2,
293        };
294        let interleaved: Vec<f32> = (0..2000).map(|i| (i as f32) * 1e-3).collect();
295
296        let mut bulk = Vec::new();
297        PcmConverter::new(fmt, 16_000)
298            .unwrap()
299            .convert(&interleaved, &mut bulk)
300            .unwrap();
301
302        // 奇数長(フレーム境界をまたぐ)で分割して流す。
303        let mut split = Vec::new();
304        let mut conv = PcmConverter::new(fmt, 16_000).unwrap();
305        for chunk in interleaved.chunks(777) {
306            conv.convert(chunk, &mut split).unwrap();
307        }
308        assert_eq!(bulk, split);
309    }
310
311    fn rms(samples: &[f32]) -> f32 {
312        let sum_sq: f64 = samples.iter().map(|&x| (x as f64) * (x as f64)).sum();
313        (sum_sq / samples.len() as f64).sqrt() as f32
314    }
315
316    fn zero_crossings(samples: &[f32]) -> usize {
317        let mut n = 0;
318        for w in samples.windows(2) {
319            if (w[0] < 0.0 && w[1] >= 0.0) || (w[0] >= 0.0 && w[1] < 0.0) {
320                n += 1;
321            }
322        }
323        n
324    }
325}