Skip to main content

flexaudio_core/
normalizer.rs

1//! 任意のデバイスフレーム(任意 SR / 任意 ch / interleaved f32)を 2 段で
2//! 正規化・再変換する。
3//!
4//! ```text
5//! 入力(任意 SR/ch)
6//!   │  第 1 段(内部正規化・不変)
7//!   │   ・チャンネル mix(→stereo)
8//!   │   ・SR 変換(rubato, →48000)
9//!   ▼
10//! 内部正規形: f32 / 48000 Hz / stereo / 20ms = 960 frame
11//!   │  第 2 段(出口・新規)
12//!   │   ・チャンネル変換(stereo→mono 平均 / mono→stereo 複製 / そのまま)
13//!   │   ・SR 変換(rubato, 48000→output.sample_rate。等しければパススルー)
14//!   ▼
15//! 出力: f32 / output.sample_rate / output.channels / 時間ベース 20ms 固定
16//!        (48k=960 / 16k=320 / 8k=160 frame)
17//! ```
18//!
19//! 既定の出力 `{48000, 2}` なら第 2 段は丸ごとパススルー(内部正規形がそのまま出る)。
20//! 第 1 段の SR 変換は `in_sample_rate == 48000` で、第 2 段の SR 変換は
21//! `output.sample_rate == 48000` でそれぞれパススルーになる。
22//!
23//! どちらの rubato リサンプラも `FixedAsync::Input`(固定入力チャンク)で、生成された
24//! 可変長出力を内部 accumulator に集約し、20ms 相当の境界で切り出す。端数はリサンプラ
25//! 内部と accumulator が次へ持ち越す。
26//!
27//! PTS は出力チャンク先頭サンプルに対応する device_pts を、入力→出力サンプルオフセット
28//! の比で追跡して割り当てる。seq はストリーム層が付与する。
29
30use rubato::audioadapter_buffers::direct::InterleavedSlice;
31use rubato::{
32    Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, SincInterpolationType,
33    WindowFunction,
34};
35
36use crate::types::{Error, OutputFormat, Result, CHANNELS, SAMPLE_RATE};
37
38/// 内部正規形 1 チャンクのフレーム数(20ms @ 48kHz)。第 1 段の切り出し境界。
39pub const CHUNK_FRAMES: usize = 960;
40
41/// 内部正規形のチャンネル数(stereo)。
42const INNER_CH: usize = CHANNELS as usize; // 2
43
44/// 入力デバイスフレームを内部正規形(48k/stereo/960frame)へ正規化し、さらに
45/// 出力フォーマット(`output.sample_rate` / `output.channels` / 時間ベース 20ms)
46/// へ再変換するステートフルな 2 段変換器。
47///
48/// `push` で interleaved サンプルを蓄積し、`pop_chunk` で完成済みの出力チャンクを
49/// 1 つずつ取り出す。
50pub struct Normalizer {
51    in_sample_rate: u32,
52    in_channels: usize,
53    output: OutputFormat,
54
55    // --- 第 1 段(内部正規化: → 48k/stereo) ---
56    /// 48000 入力ならパススルー(リサンプラ無し)。
57    stage1_resampler: Option<ResamplerState>,
58    /// 第 1 段出力(内部正規形 stereo interleaved)。960 frame 境界で第 2 段へ渡す。
59    inner_buf: Vec<f32>,
60
61    // --- 第 2 段(出口: 48k/stereo → output) ---
62    /// 出力段。`output == {48000, 2}` なら `None`(完全パススルー)。
63    stage2: Option<OutputStage>,
64    /// 完成待ちの出力(output.channels の interleaved)。`pop_chunk` がここから切る。
65    out_buf: Vec<f32>,
66    /// 出力 1 チャンクのフレーム数(`output.chunk_frames()`)。
67    out_chunk_frames: usize,
68    /// 出力チャンネル数。
69    out_channels: usize,
70
71    /// `out_buf` 先頭(まだ pop していない最古サンプル)に対応する出力フレーム索引。
72    out_frame_origin: u64,
73
74    /// PTS アンカー: ある出力フレーム索引に device_pts(ns) を結び付ける。
75    pts_anchor: Option<PtsAnchor>,
76
77    /// これまでに第 2 段から生成した累計出力フレーム数(アンカー計算用)。
78    total_out_frames: u64,
79    /// これまでに第 1 段が生成した累計内部 48k フレーム数(アンカー計算用)。
80    total_inner_frames: u64,
81}
82
83#[derive(Clone, Copy)]
84struct PtsAnchor {
85    /// 出力フレーム索引(出力レート基準)。
86    out_frame: u64,
87    /// その出力フレームに対応する device_pts(ns)。
88    pts_ns: i64,
89}
90
91/// rubato `Async`(`FixedAsync::Input`)を 1 段ぶん束ねた SR 変換器。
92///
93/// 固定入力チャンク `chunk_in_frames` ごとに `process` し、可変長出力を
94/// `out_buf`(呼び出し側 accumulator)へ追記する。`channels` は段によって
95/// 異なる(第 1 段は常に stereo=2、第 2 段は出力チャンネル数)。
96struct ResamplerState {
97    inner: Async<f32>,
98    channels: usize,
99    /// rubato が要求する 1 回分の入力フレーム数(`FixedAsync::Input` で固定)。
100    chunk_in_frames: usize,
101    /// 1 回の `process` が生成しうる最大出力フレーム数。
102    max_out_frames: usize,
103    /// 未処理の入力(interleaved・`channels` ch)。
104    in_accum: Vec<f32>,
105    /// rubato への出力スクラッチ(再利用してアロケートを避ける)。
106    out_scratch: Vec<f32>,
107}
108
109/// 第 2 段(出口)。内部正規形 48k/stereo の 960frame チャンクを受け、
110/// チャンネル変換 → SR 変換して出力フォーマットの interleaved を生成する。
111struct OutputStage {
112    out_channels: usize,
113    /// 48000 → output.sample_rate のリサンプラ。`output.sample_rate == 48000`
114    /// なら `None`(SR パススルー)。チャンネル変換後のサンプルに適用する。
115    resampler: Option<ResamplerState>,
116    /// チャンネル変換後・SR 変換前のスクラッチ(48k / out_channels interleaved)。
117    ch_scratch: Vec<f32>,
118}
119
120impl Normalizer {
121    /// 入力 SR / 入力チャンネル数 / 出力フォーマットを指定して正規化器を作る。
122    ///
123    /// 第 1 段は入力を 48k/stereo へ正規化する(`in_sample_rate == 48000` なら SR
124    /// パススルー、`in_channels` が 1 なら mono→stereo 複製、2 はそのまま、3 以上は
125    /// フロント 2ch を採る)。第 2 段は内部正規形を `output` へ再変換する
126    /// (`output == {48000, 2}` ならパススルー)。
127    ///
128    /// `output` は呼び出し側で [`OutputFormat::validate`] 済みであることを期待する
129    /// (ここでは妥当域へ丸めない)。
130    ///
131    /// rubato リサンプラの構築は極端なレート比などで失敗し得る。panic させると非 RT
132    /// の取り込みスレッドが無言で止まるため、失敗時は [`Error::Backend`] を返して
133    /// 呼び出し側に伝播させる。
134    pub fn new(in_sample_rate: u32, in_channels: u16, output: OutputFormat) -> Result<Self> {
135        let in_channels = in_channels.max(1) as usize;
136
137        // 第 1 段リサンプラ(→48000)。
138        let stage1_resampler = if in_sample_rate == SAMPLE_RATE {
139            None
140        } else {
141            Some(ResamplerState::new(in_sample_rate, SAMPLE_RATE, INNER_CH)?)
142        };
143
144        let out_channels = (output.channels.max(1)) as usize;
145        let out_chunk_frames = output.chunk_frames().max(1);
146
147        // 第 2 段。出力が内部正規形と完全一致なら不要(パススルー)。
148        let stage2 = if output.sample_rate == SAMPLE_RATE && out_channels == INNER_CH {
149            None
150        } else {
151            Some(OutputStage::new(output.sample_rate, out_channels)?)
152        };
153
154        Ok(Self {
155            in_sample_rate,
156            in_channels,
157            output,
158            stage1_resampler,
159            inner_buf: Vec::with_capacity(CHUNK_FRAMES * INNER_CH * 4),
160            stage2,
161            out_buf: Vec::with_capacity(out_chunk_frames * out_channels * 4),
162            out_chunk_frames,
163            out_channels,
164            out_frame_origin: 0,
165            pts_anchor: None,
166            total_out_frames: 0,
167            total_inner_frames: 0,
168        })
169    }
170
171    /// 入力サンプルレート(Hz)。
172    pub fn in_sample_rate(&self) -> u32 {
173        self.in_sample_rate
174    }
175
176    /// 出力フォーマット。
177    pub fn output(&self) -> OutputFormat {
178        self.output
179    }
180
181    /// 第 1 段 SR 変換がパススルー(in == 48000)か。
182    pub fn is_passthrough(&self) -> bool {
183        self.stage1_resampler.is_none()
184    }
185
186    /// 第 2 段(出口)が完全パススルー(output == {48000, 2})か。
187    pub fn is_output_passthrough(&self) -> bool {
188        self.stage2.is_none()
189    }
190
191    /// interleaved 入力サンプルを蓄積する。
192    ///
193    /// `interleaved` の長さは `in_channels` の倍数であること。`device_pts_ns` は
194    /// この push の先頭フレームに対応するデバイス由来 PTS。
195    ///
196    /// rubato の `process` が失敗したら [`Error::Backend`] を返す(panic させて取り込み
197    /// スレッドを無言で止めない。呼び出し側がストリームを明示停止できる)。
198    pub fn push(&mut self, interleaved: &[f32], device_pts_ns: i64) -> Result<()> {
199        if interleaved.is_empty() {
200            return Ok(());
201        }
202        let in_frames = interleaved.len() / self.in_channels;
203        if in_frames == 0 {
204            return Ok(());
205        }
206
207        // この push 先頭が将来現れる出力フレーム位置を比で近似して PTS アンカーを更新する
208        // (リサンプラ内部の保持端数があるため近似)。
209        self.update_pts_anchor(device_pts_ns);
210
211        // 第 1 段: チャンネル mix → stereo interleaved。
212        let mut stereo = Vec::with_capacity(in_frames * INNER_CH);
213        Self::mix_to_stereo(interleaved, self.in_channels, in_frames, &mut stereo);
214
215        match &mut self.stage1_resampler {
216            None => {
217                // SR パススルー。そのまま内部 accumulator へ。
218                self.total_inner_frames += in_frames as u64;
219                self.inner_buf.extend_from_slice(&stereo);
220            }
221            Some(rs) => {
222                rs.in_accum.extend_from_slice(&stereo);
223                rs.drain_into(&mut self.inner_buf, &mut self.total_inner_frames)?;
224            }
225        }
226
227        // 内部正規形が 960 frame 溜まるごとに第 2 段へ流す。
228        self.pump_stage2()
229    }
230
231    /// 完成済みの出力チャンクを 1 つ取り出す。
232    ///
233    /// 返り値は `(output.channels interleaved の `out_chunk_frames` frame, 先頭サンプル
234    /// の device_pts(ns))`。1 チャンク分溜まっていなければ `None`。
235    pub fn pop_chunk(&mut self) -> Option<(Vec<f32>, i64)> {
236        let need = self.out_chunk_frames * self.out_channels;
237        if self.out_buf.len() < need {
238            return None;
239        }
240
241        let pts = self.pts_for_out_frame(self.out_frame_origin);
242
243        let chunk: Vec<f32> = self.out_buf.drain(..need).collect();
244        self.out_frame_origin += self.out_chunk_frames as u64;
245
246        Some((chunk, pts))
247    }
248
249    /// 現在 `out_buf` に溜まっている未取り出し出力フレーム数。
250    pub fn buffered_out_frames(&self) -> usize {
251        self.out_buf.len() / self.out_channels
252    }
253
254    // --- 内部ヘルパ ---
255
256    /// `inner_buf` に溜まった内部正規形を 960 frame 境界で第 2 段へ流し、生成された
257    /// 出力フレームを `out_buf` へ追記する。
258    fn pump_stage2(&mut self) -> Result<()> {
259        let inner_chunk = CHUNK_FRAMES * INNER_CH;
260
261        match &mut self.stage2 {
262            None => {
263                // 第 2 段パススルー。内部正規形がそのまま出力なので inner_buf を out_buf へ
264                // 渡すだけでよい(境界揃えは不要。pop_chunk が 960 単位で切る)。
265                if !self.inner_buf.is_empty() {
266                    let n_frames = (self.inner_buf.len() / INNER_CH) as u64;
267                    self.total_out_frames += n_frames;
268                    self.out_buf.append(&mut self.inner_buf);
269                }
270            }
271            Some(stage) => {
272                while self.inner_buf.len() >= inner_chunk {
273                    // 借用衝突を避けるため 1 チャンク分をローカルへ取り出す。
274                    let chunk: Vec<f32> = self.inner_buf.drain(..inner_chunk).collect();
275                    stage.process_inner_chunk(
276                        &chunk,
277                        &mut self.out_buf,
278                        &mut self.total_out_frames,
279                    )?;
280                }
281            }
282        }
283        Ok(())
284    }
285
286    /// 任意 ch interleaved を stereo interleaved へ mix して `dst` に push する。
287    fn mix_to_stereo(src: &[f32], in_ch: usize, in_frames: usize, dst: &mut Vec<f32>) {
288        match in_ch {
289            1 => {
290                // mono → stereo(L=R 複製)
291                for &s in &src[..in_frames] {
292                    dst.push(s);
293                    dst.push(s);
294                }
295            }
296            2 => {
297                // 2ch はそのまま(必要分のみ)
298                dst.extend_from_slice(&src[..in_frames * 2]);
299            }
300            _ => {
301                // >2ch は当面フロント 2ch を採る。
302                // TODO(BS.775): 5.1 等の正式なダウンミックス係数を適用する。
303                for f in 0..in_frames {
304                    let base = f * in_ch;
305                    dst.push(src[base]);
306                    dst.push(src[base + 1]);
307                }
308            }
309        }
310    }
311
312    /// この push 先頭に対応する出力フレーム位置へ PTS アンカーを張る。
313    ///
314    /// 出力フレーム位置は累計入力フレーム数を入力 SR → 出力 SR 比で写像した近似値
315    /// (両段のリサンプラ内部の保持端数があるため厳密ではない)。
316    fn update_pts_anchor(&mut self, device_pts_ns: i64) {
317        // 入力 → 出力レート比(第 1 段 in→48000 と第 2 段 48000→output を合成)。
318        let ratio = self.output.sample_rate as f64 / self.in_sample_rate as f64;
319        let projected_out_frame = (self.total_in_frames_estimate() as f64 * ratio) as u64;
320        self.pts_anchor = Some(PtsAnchor {
321            out_frame: projected_out_frame,
322            pts_ns: device_pts_ns,
323        });
324    }
325
326    /// PTS アンカー用の累計入力フレーム数の推定。
327    ///
328    /// `total_inner_frames` は内部 48k 基準なので、入力レート基準へ戻して返す
329    /// (`update_pts_anchor` がこれを出力レートへ写像する)。
330    fn total_in_frames_estimate(&self) -> u64 {
331        let inv = self.in_sample_rate as f64 / SAMPLE_RATE as f64;
332        (self.total_inner_frames as f64 * inv) as u64
333    }
334
335    /// 出力フレーム索引 `out_frame` に対応する device_pts(ns) を、
336    /// アンカーから出力レート比で外挿して求める。
337    fn pts_for_out_frame(&self, out_frame: u64) -> i64 {
338        match self.pts_anchor {
339            None => crate::clock::monotonic_now_ns(),
340            Some(anchor) => {
341                let frame_delta = out_frame as i64 - anchor.out_frame as i64;
342                let ns_per_out_frame = 1_000_000_000_i64 / self.output.sample_rate as i64;
343                anchor.pts_ns + frame_delta * ns_per_out_frame
344            }
345        }
346    }
347}
348
349impl ResamplerState {
350    /// `in_sr` → `out_sr` の固定比リサンプラを `channels` ch で作る。
351    ///
352    /// rubato の構築失敗時は [`Error::Backend`] を返す(panic させてスレッドを無言で
353    /// 止めない)。
354    fn new(in_sr: u32, out_sr: u32, channels: usize) -> Result<Self> {
355        let ratio = out_sr as f64 / in_sr as f64;
356        // 固定入力チャンクは 20ms 相当の入力フレーム(端数は rubato が内部に保持する)。
357        let chunk_in_frames = (in_sr as usize / 50).max(64);
358
359        let params = SincInterpolationParameters {
360            sinc_len: 128,
361            f_cutoff: 0.95,
362            interpolation: SincInterpolationType::Linear,
363            oversampling_factor: 128,
364            window: WindowFunction::BlackmanHarris2,
365        };
366
367        let inner = Async::<f32>::new_sinc(
368            ratio,
369            1.0, // 比は固定(可変リサンプルは不要)
370            &params,
371            chunk_in_frames,
372            channels,
373            FixedAsync::Input,
374        )
375        .map_err(|e| Error::Backend(format!("rubato sinc resampler construction failed: {e}")))?;
376
377        let max_out_frames = inner.output_frames_max();
378
379        Ok(Self {
380            inner,
381            channels,
382            chunk_in_frames,
383            max_out_frames,
384            in_accum: Vec::with_capacity(chunk_in_frames * channels * 4),
385            out_scratch: vec![0.0; max_out_frames * channels],
386        })
387    }
388
389    /// `in_accum` に溜まった分を chunk_in_frames 単位で可能な限りリサンプルし、生成した
390    /// interleaved を `out_buf` へ追記する。
391    ///
392    /// rubato の adapter 構築・`process_into_buffer` が失敗したら [`Error::Backend`]
393    /// を返す(panic させて取り込みスレッドを無言で止めない)。
394    fn drain_into(&mut self, out_buf: &mut Vec<f32>, total_out_frames: &mut u64) -> Result<()> {
395        let step = self.chunk_in_frames * self.channels;
396
397        while self.in_accum.len() >= step {
398            let in_adapter =
399                InterleavedSlice::new(&self.in_accum[..step], self.channels, self.chunk_in_frames)
400                    .map_err(|e| {
401                        Error::Backend(format!("rubato interleaved input adapter failed: {e}"))
402                    })?;
403
404            let mut out_adapter = InterleavedSlice::new_mut(
405                &mut self.out_scratch[..],
406                self.channels,
407                self.max_out_frames,
408            )
409            .map_err(|e| {
410                Error::Backend(format!("rubato interleaved output adapter failed: {e}"))
411            })?;
412
413            let indexing = Indexing {
414                input_offset: 0,
415                output_offset: 0,
416                partial_len: None,
417                active_channels_mask: None,
418            };
419
420            let (_in_used, out_written) = self
421                .inner
422                .process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
423                .map_err(|e| Error::Backend(format!("rubato process_into_buffer failed: {e}")))?;
424
425            let n_samples = out_written * self.channels;
426            out_buf.extend_from_slice(&self.out_scratch[..n_samples]);
427            *total_out_frames += out_written as u64;
428
429            // 消費した入力を取り除く(FixedAsync::Input なので消費は chunk_in_frames 固定)。
430            self.in_accum.drain(..step);
431        }
432        Ok(())
433    }
434}
435
436impl OutputStage {
437    /// 出力レート / 出力チャンネル数を指定して出口段を作る。
438    ///
439    /// `out_sample_rate == 48000` なら SR 変換はパススルー(チャンネル変換のみ)。
440    /// rubato 構築失敗は [`Error::Backend`] として伝播する。
441    fn new(out_sample_rate: u32, out_channels: usize) -> Result<Self> {
442        let resampler = if out_sample_rate == SAMPLE_RATE {
443            None
444        } else {
445            // 内部正規形 48000 から out_sample_rate へ、out_channels ch で変換する。
446            Some(ResamplerState::new(
447                SAMPLE_RATE,
448                out_sample_rate,
449                out_channels,
450            )?)
451        };
452        Ok(Self {
453            out_channels,
454            resampler,
455            ch_scratch: Vec::with_capacity(CHUNK_FRAMES * out_channels),
456        })
457    }
458
459    /// 内部正規形 1 チャンク(48k/stereo・960 frame interleaved)を処理して、出力
460    /// フォーマットの interleaved を `out_buf` へ追記する。
461    fn process_inner_chunk(
462        &mut self,
463        inner_stereo: &[f32],
464        out_buf: &mut Vec<f32>,
465        total_out_frames: &mut u64,
466    ) -> Result<()> {
467        debug_assert_eq!(inner_stereo.len(), CHUNK_FRAMES * INNER_CH);
468
469        // チャンネル変換: stereo → out_channels。
470        self.ch_scratch.clear();
471        match self.out_channels {
472            1 => {
473                // stereo → mono(L/R 平均)。
474                for f in 0..CHUNK_FRAMES {
475                    let l = inner_stereo[f * 2];
476                    let r = inner_stereo[f * 2 + 1];
477                    self.ch_scratch.push((l + r) * 0.5);
478                }
479            }
480            2 => {
481                self.ch_scratch.extend_from_slice(inner_stereo);
482            }
483            _ => {
484                // validate で 1/2 に絞られているはず。届いても L 複製で凌ぐ。
485                for f in 0..CHUNK_FRAMES {
486                    let l = inner_stereo[f * 2];
487                    for _ in 0..self.out_channels {
488                        self.ch_scratch.push(l);
489                    }
490                }
491            }
492        }
493
494        // SR 変換: 48000 → out_sample_rate。パススルーなら ch_scratch をそのまま出力へ。
495        match &mut self.resampler {
496            None => {
497                let n_frames = (self.ch_scratch.len() / self.out_channels) as u64;
498                *total_out_frames += n_frames;
499                out_buf.extend_from_slice(&self.ch_scratch);
500            }
501            Some(rs) => {
502                rs.in_accum.extend_from_slice(&self.ch_scratch);
503                rs.drain_into(out_buf, total_out_frames)?;
504            }
505        }
506        Ok(())
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use std::f32::consts::PI;
514
515    /// 既定出力({48000, 2})のヘルパ。
516    fn default_out() -> OutputFormat {
517        OutputFormat::default()
518    }
519
520    #[test]
521    fn mono_48k_to_stereo_duplicates_channels() {
522        let mut n = Normalizer::new(48_000, 1, default_out()).expect("normalizer");
523        assert!(n.is_passthrough());
524        assert!(n.is_output_passthrough());
525        // 960 フレーム分の mono 入力(パススルーなので 1 チャンクちょうど)。
526        let mono: Vec<f32> = (0..CHUNK_FRAMES).map(|i| (i as f32) * 0.001).collect();
527        n.push(&mono, 0).expect("push");
528        let (chunk, _pts) = n.pop_chunk().expect("one chunk");
529        assert_eq!(chunk.len(), CHUNK_FRAMES * 2);
530        // L == R がフレーム毎に成立。
531        for f in 0..CHUNK_FRAMES {
532            assert_eq!(chunk[f * 2], chunk[f * 2 + 1], "L==R at frame {f}");
533            assert_eq!(chunk[f * 2], mono[f]);
534        }
535    }
536
537    #[test]
538    fn passthrough_preserves_frame_count() {
539        let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
540        assert!(n.is_passthrough());
541        assert!(n.is_output_passthrough());
542        // 2 チャンク分 + 端数。
543        let frames = CHUNK_FRAMES * 2 + 100;
544        let stereo: Vec<f32> = (0..frames * 2).map(|i| (i as f32) * 1e-4).collect();
545        n.push(&stereo, 0).expect("push");
546
547        let mut got_frames = 0usize;
548        while let Some((c, _)) = n.pop_chunk() {
549            assert_eq!(c.len(), CHUNK_FRAMES * 2);
550            got_frames += CHUNK_FRAMES;
551        }
552        // ちょうど 2 チャンク取り出せ、端数 100 frame は残る。
553        assert_eq!(got_frames, CHUNK_FRAMES * 2);
554        assert_eq!(n.buffered_out_frames(), 100);
555    }
556
557    #[test]
558    fn stereo_44100_to_48000_yields_about_50_chunks_per_second() {
559        let mut n = Normalizer::new(44_100, 2, default_out()).expect("normalizer");
560        assert!(!n.is_passthrough());
561
562        // 1 秒分の 44100Hz ステレオ サイン波。
563        let in_frames = 44_100;
564        let freq = 440.0_f32;
565        let mut interleaved = Vec::with_capacity(in_frames * 2);
566        for i in 0..in_frames {
567            let s = (2.0 * PI * freq * (i as f32) / 44_100.0).sin() * 0.5;
568            interleaved.push(s); // L
569            interleaved.push(s); // R
570        }
571
572        // 細切れ push(実機の小バッファ到着を模す)でも panic しないこと。
573        let mut pts = 0i64;
574        for block in interleaved.chunks(441 * 2) {
575            n.push(block, pts).expect("push");
576            pts += (block.len() as i64 / 2) * 1_000_000_000 / 44_100;
577        }
578
579        let mut chunks = 0usize;
580        while let Some((c, _pts)) = n.pop_chunk() {
581            assert_eq!(c.len(), CHUNK_FRAMES * 2);
582            chunks += 1;
583        }
584        assert!(
585            (47..=50).contains(&chunks),
586            "expected ~50 chunks, got {chunks}"
587        );
588    }
589
590    #[test]
591    fn pts_increases_monotonically_across_chunks() {
592        let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
593        let frames = CHUNK_FRAMES * 3;
594        let stereo = vec![0.0f32; frames * 2];
595        n.push(&stereo, 100_000_000).expect("push");
596
597        let mut last = i64::MIN;
598        let mut count = 0;
599        while let Some((_, pts)) = n.pop_chunk() {
600            assert!(pts >= last, "pts must be non-decreasing");
601            last = pts;
602            count += 1;
603        }
604        assert_eq!(count, 3);
605    }
606
607    // --- 第 2 段(出口)の検証 ---
608
609    /// 48k/stereo 入力 + 出力 {16000, 1} → 320 frame の mono チャンク。
610    #[test]
611    fn output_16k_mono_yields_320_frame_mono_chunks() {
612        let out = OutputFormat {
613            sample_rate: 16_000,
614            channels: 1,
615        };
616        let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
617        assert!(n.is_passthrough()); // 第 1 段は SR パススルー(48k 入力)。
618        assert!(!n.is_output_passthrough()); // 第 2 段は有効。
619
620        // 1 秒分の 48k stereo サイン波(細切れ push)。
621        let in_frames = 48_000;
622        let freq = 440.0_f32;
623        let mut pts = 0i64;
624        for blk in 0..(in_frames / 480) {
625            let mut block = Vec::with_capacity(480 * 2);
626            for j in 0..480 {
627                let i = blk * 480 + j;
628                let s = (2.0 * PI * freq * (i as f32) / 48_000.0).sin() * 0.5;
629                block.push(s);
630                block.push(s);
631            }
632            n.push(&block, pts).expect("push");
633            pts += 480 * 1_000_000_000 / 48_000;
634        }
635
636        let mut chunks = 0usize;
637        while let Some((c, _)) = n.pop_chunk() {
638            assert_eq!(c.len(), 320, "16k mono 20ms = 320 sample (mono)");
639            chunks += 1;
640        }
641        // 16000/320 = 50 チャンク/秒。リサンプラ遅延で約 50。
642        assert!(
643            (47..=50).contains(&chunks),
644            "expected ~50 chunks, got {chunks}"
645        );
646    }
647
648    /// 出力 {16000, 2} → 320 frame・640 sample(stereo)。
649    #[test]
650    fn output_16k_stereo_yields_320_frame_640_sample_chunks() {
651        let out = OutputFormat {
652            sample_rate: 16_000,
653            channels: 2,
654        };
655        let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
656        let in_frames = 48_000;
657        let stereo: Vec<f32> = (0..in_frames * 2)
658            .map(|i| ((i / 2) as f32 * 0.0001).sin() * 0.3)
659            .collect();
660        for block in stereo.chunks(480 * 2) {
661            n.push(block, 0).expect("push");
662        }
663        let mut chunks = 0usize;
664        while let Some((c, _)) = n.pop_chunk() {
665            assert_eq!(c.len(), 640, "16k stereo 20ms = 320 frame * 2 = 640 sample");
666            chunks += 1;
667        }
668        assert!(
669            (47..=50).contains(&chunks),
670            "expected ~50 chunks, got {chunks}"
671        );
672    }
673
674    /// 出力 {8000, 2} → 160 frame・320 sample。
675    #[test]
676    fn output_8k_stereo_yields_160_frame_chunks() {
677        let out = OutputFormat {
678            sample_rate: 8_000,
679            channels: 2,
680        };
681        let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
682        let stereo: Vec<f32> = (0..48_000 * 2)
683            .map(|i| (i as f32 * 1e-5).sin() * 0.2)
684            .collect();
685        for block in stereo.chunks(480 * 2) {
686            n.push(block, 0).expect("push");
687        }
688        let mut chunks = 0usize;
689        while let Some((c, _)) = n.pop_chunk() {
690            assert_eq!(c.len(), 320, "8k stereo 20ms = 160 frame * 2 = 320 sample");
691            chunks += 1;
692        }
693        assert!(
694            (47..=50).contains(&chunks),
695            "expected ~50 chunks, got {chunks}"
696        );
697    }
698
699    /// stereo→mono は L/R 平均(L=+a, R=-a の逆相は 0 に近づく)。
700    #[test]
701    fn stereo_to_mono_is_lr_average() {
702        // 出力 48000/mono にして SR パススルー・チャンネル変換のみを検証する。
703        let out = OutputFormat {
704            sample_rate: 48_000,
705            channels: 1,
706        };
707        let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
708        // 完全逆相(L=+0.5, R=-0.5)→ 平均 0。
709        let mut stereo = Vec::with_capacity(CHUNK_FRAMES * 2);
710        for _ in 0..CHUNK_FRAMES {
711            stereo.push(0.5);
712            stereo.push(-0.5);
713        }
714        n.push(&stereo, 0).expect("push");
715        let (chunk, _) = n.pop_chunk().expect("one mono chunk");
716        assert_eq!(chunk.len(), CHUNK_FRAMES); // mono 960 sample。
717        for &s in &chunk {
718            assert!(s.abs() < 1e-6, "逆相の平均は 0 付近のはず: {s}");
719        }
720    }
721
722    // --- 値検証ヘルパ(振幅・周波数の保存を確認する) ---
723
724    /// サンプル列の RMS(線形)。正弦波なら振幅 A に対し A/√2 になる。
725    fn rms(samples: &[f32]) -> f32 {
726        if samples.is_empty() {
727            return 0.0;
728        }
729        let sum_sq: f64 = samples.iter().map(|&x| (x as f64) * (x as f64)).sum();
730        (sum_sq / samples.len() as f64).sqrt() as f32
731    }
732
733    /// 正→負 / 負→正 のゼロ交差回数を数える。1 周期で 2 回交差するので、
734    /// 推定周波数 = (交差数 / 2) / 秒数。先頭/末尾の過渡を避けて中央を渡すこと。
735    fn zero_crossings(samples: &[f32]) -> usize {
736        let mut crossings = 0;
737        for w in samples.windows(2) {
738            // 厳密な符号反転のみ(0 ちょうどは無視)。
739            if (w[0] < 0.0 && w[1] >= 0.0) || (w[0] >= 0.0 && w[1] < 0.0) {
740                crossings += 1;
741            }
742        }
743        crossings
744    }
745
746    /// 44.1kHz/mono 440Hz 正弦を 48kHz/stereo へリサンプルしても、振幅(RMS)と
747    /// 周波数(ゼロ交差推定)が保存される。リサンプラのリンギングを避けるため
748    /// 1 秒ぶん流して中央のチャンク群だけで測る。
749    #[test]
750    fn resample_44100_to_48000_preserves_amplitude_and_frequency() {
751        let mut n = Normalizer::new(44_100, 1, default_out()).expect("normalizer");
752        let freq = 440.0_f32;
753        let amp = 0.5_f32;
754        let in_rate = 44_100usize;
755        // 2 秒ぶん流して十分なチャンクを得る(過渡を捨てる余裕を持つ)。
756        let total_frames = in_rate * 2;
757        let mut pts = 0i64;
758        for blk in 0..(total_frames / 441) {
759            let mut block = Vec::with_capacity(441);
760            for j in 0..441 {
761                let i = blk * 441 + j;
762                block.push((2.0 * PI * freq * (i as f32) / in_rate as f32).sin() * amp);
763            }
764            n.push(&block, pts).expect("push");
765            pts += 441 * 1_000_000_000 / in_rate as i64;
766        }
767
768        // 全チャンクを連結(出力は 48k/stereo/960frame)。
769        let mut left: Vec<f32> = Vec::new();
770        while let Some((c, _)) = n.pop_chunk() {
771            assert_eq!(c.len(), CHUNK_FRAMES * 2);
772            // L チャンネルだけ取り出す(mono→stereo 複製なので L==R)。
773            for f in 0..CHUNK_FRAMES {
774                assert_eq!(c[f * 2], c[f * 2 + 1], "mono 入力なので L==R");
775                left.push(c[f * 2]);
776            }
777        }
778        assert!(left.len() >= 48_000, "1 秒以上の出力が必要: {}", left.len());
779
780        // 過渡(先頭・末尾各 0.25 秒 = 12000 sample)を捨てて中央 1 秒で測る。
781        let start = 12_000;
782        let mid = &left[start..start + 48_000];
783
784        // 振幅: 正弦の RMS は amp/√2 ≈ 0.3536。リサンプラ通過で ±5% 以内。
785        let got_rms = rms(mid);
786        let expect_rms = amp / std::f32::consts::SQRT_2;
787        let rms_err = ((got_rms - expect_rms) / expect_rms).abs();
788        assert!(
789            rms_err < 0.05,
790            "RMS 保存誤差が大きい: got={got_rms} expect={expect_rms} err={rms_err}"
791        );
792
793        // 周波数: 中央 1 秒(48000 sample)のゼロ交差 ≈ 2*440 = 880。±2% 以内。
794        let crossings = zero_crossings(mid);
795        let est_freq = crossings as f32 / 2.0; // 1 秒なので交差数/2 = Hz。
796        let freq_err = ((est_freq - freq) / freq).abs();
797        assert!(
798            freq_err < 0.02,
799            "周波数 保存誤差が大きい: 交差={crossings} 推定={est_freq}Hz err={freq_err}"
800        );
801    }
802
803    /// 16k/mono 出力の実チャンネル数とサンプル値: 48k/stereo 440Hz 入力を
804    /// 16k/mono へ落としても 1ch・320sample で振幅/周波数が保存される。
805    #[test]
806    fn output_16k_mono_preserves_values() {
807        let out = OutputFormat {
808            sample_rate: 16_000,
809            channels: 1,
810        };
811        let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
812        let freq = 440.0_f32;
813        let amp = 0.5_f32;
814        let in_rate = 48_000usize;
815        let total_frames = in_rate * 2;
816        let mut pts = 0i64;
817        for blk in 0..(total_frames / 480) {
818            let mut block = Vec::with_capacity(480 * 2);
819            for j in 0..480 {
820                let i = blk * 480 + j;
821                let s = (2.0 * PI * freq * (i as f32) / in_rate as f32).sin() * amp;
822                block.push(s); // L
823                block.push(s); // R
824            }
825            n.push(&block, pts).expect("push");
826            pts += 480 * 1_000_000_000 / in_rate as i64;
827        }
828
829        let mut mono: Vec<f32> = Vec::new();
830        while let Some((c, _)) = n.pop_chunk() {
831            assert_eq!(c.len(), 320, "16k/mono 20ms = 320 sample(1ch)");
832            mono.extend_from_slice(&c);
833        }
834        assert!(mono.len() >= 16_000, "1 秒以上必要: {}", mono.len());
835
836        // 過渡を捨てて中央 1 秒(16000 sample)で測る。
837        let start = 4_000;
838        let mid = &mono[start..start + 16_000];
839
840        // L==R の同相信号を平均してもレベル不変 → RMS ≈ amp/√2。
841        let got_rms = rms(mid);
842        let expect_rms = amp / std::f32::consts::SQRT_2;
843        let rms_err = ((got_rms - expect_rms) / expect_rms).abs();
844        assert!(
845            rms_err < 0.05,
846            "16k/mono RMS 保存誤差: got={got_rms} expect={expect_rms} err={rms_err}"
847        );
848
849        // 周波数: 16000 sample の中央 1 秒で交差 ≈ 880。±2% 以内。
850        let est_freq = zero_crossings(mid) as f32 / 2.0;
851        let freq_err = ((est_freq - freq) / freq).abs();
852        assert!(
853            freq_err < 0.02,
854            "16k/mono 周波数 保存誤差: 推定={est_freq}Hz err={freq_err}"
855        );
856    }
857
858    /// PTS は単調増加し、隣接チャンク間の delta が ~20ms(1e7 ns ±許容)になる。
859    /// 48k パススルー経路で PTS アンカーが正しく外挿されることを値で確認する。
860    #[test]
861    fn pts_delta_is_about_20ms_between_chunks() {
862        let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
863        // 480 frame(10ms)ずつ pts 付きで push(実機の小バッファ到着を模す)。
864        let mut device_pts = 1_000_000_000i64; // 任意の原点。
865        let block_frames = 480usize;
866        for _ in 0..20 {
867            let stereo = vec![0.1f32; block_frames * 2];
868            n.push(&stereo, device_pts).expect("push");
869            device_pts += block_frames as i64 * 1_000_000_000 / 48_000;
870        }
871
872        let mut pts_list = Vec::new();
873        while let Some((_, pts)) = n.pop_chunk() {
874            pts_list.push(pts);
875        }
876        assert!(
877            pts_list.len() >= 5,
878            "十分なチャンク数が必要: {}",
879            pts_list.len()
880        );
881
882        // 20ms = 20_000_000 ns。許容 ±5%(1e6 ns)。
883        for w in pts_list.windows(2) {
884            let delta = w[1] - w[0];
885            assert!(delta > 0, "PTS は厳密に増加: {} -> {}", w[0], w[1]);
886            assert!(
887                (delta - 20_000_000).abs() <= 1_000_000,
888                "隣接 PTS delta が ~20ms でない: {delta} ns"
889            );
890        }
891    }
892
893    /// 入力サンプルが空 / 端数(in_channels の倍数未満)でも panic せず Ok を返し、
894    /// チャンクは生成されない(境界・防御)。
895    #[test]
896    fn push_empty_and_subframe_are_noops() {
897        let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
898        // 空。
899        n.push(&[], 0).expect("empty push ok");
900        // stereo(2ch) なのに 1 サンプルだけ → in_frames=0 で早期 return。
901        n.push(&[0.5], 0).expect("subframe push ok");
902        assert!(n.pop_chunk().is_none(), "端数だけでは 1 チャンクも出ない");
903        assert_eq!(n.buffered_out_frames(), 0);
904    }
905
906    /// 周波数 0(無音 DC)入力は出力も全 0(peak/rms 0 経路の裏取り)。
907    #[test]
908    fn silence_input_yields_zero_output() {
909        let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
910        let stereo = vec![0.0f32; CHUNK_FRAMES * 2];
911        n.push(&stereo, 0).expect("push");
912        let (chunk, _) = n.pop_chunk().expect("one chunk");
913        assert!(chunk.iter().all(|&s| s == 0.0), "無音入力は無音出力");
914    }
915}