blip25-mbe 0.1.0

Research MBE / IMBE / AMBE+2 vocoder family: P25 wire formats, multi-generation codecs, and parametric rate conversion. Educational use; see PATENT_NOTICE.md.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
//! Encode-side tone detector — recognises Annex T tones in 160-sample
//! PCM frames and returns the matching `(I_D, A_D)` for tone-frame
//! dispatch.
//!
//! BABA-A §2.10 specifies the tone-frame *payload* (Annex T table,
//! signature + bit layout in û₀..û₃) but per `P25_Vocoder_Implementation_Spec.md`
//! §0.0.1 the **entry criteria** for emitting a tone frame are
//! "Not prescribed by the standard" — left to the implementer. So
//! this is a DSP design choice, not a P25-IP question; clean-room
//! scope rules don't apply.
//!
//! Strategy:
//!
//! 1. Window the 160 input samples with a Hann window, compute a
//!    256-point real DFT (zero-pad).
//! 2. Find the dominant peak above an SNR floor.
//! 3. Match the peak frequency to the closest single-frequency entry
//!    in Annex T (`l1 == l2`). If no match within tolerance, return
//!    `None`.
//! 4. Map the peak's energy to the 7-bit `A_D` amplitude field via
//!    the inverse of Eq. 209 (the same exponential scaling the
//!    decoder uses).
//!
//! DTMF (two-tone) detection is out of scope for this first cut —
//! the consumer can pre-classify DTMF and call `encode_tone_frame_info`
//! directly with the right `I_D` if they want to. If real DTMF input
//! shows up in the test corpus, this module gains a `detect_dtmf`
//! sibling.

use crate::ambe_plus2_wire::frame::ANNEX_T;

/// What the tone detector returns on a hit.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ToneDetection {
    /// Annex T tone ID (`I_D`) for the matched tone.
    pub id: u8,
    /// 7-bit log-amplitude (`A_D`), mapped via Eq. 209 inverse from
    /// the detected spectral magnitude.
    pub amplitude: u8,
}

/// Number of samples per detector input frame.
pub const TONE_DETECT_FRAME: usize = 160;
/// FFT size — 256-point zero-padded DFT of the windowed 160 samples.
const FFT_SIZE: usize = 256;
/// Sample rate (constant across blip25-mbe).
const SAMPLE_RATE_HZ: f64 = 8000.0;

/// Maximum frequency error for a peak to count as matching an Annex T
/// entry. Peak resolution at 256-point DFT is ~31 Hz; ±20 Hz lets us
/// snap to the nearest Annex T entry without false matches between
/// neighbouring rows (Annex T spacing is ≥ 31 Hz in the 156–400 Hz
/// band).
const MATCH_TOLERANCE_HZ: f64 = 20.0;

/// Minimum peak-to-floor SNR (in linear power) before we declare a
/// tone. Below this the input is too noisy / broadband to be a clean
/// tone; emit a voice frame instead.
const SNR_THRESHOLD: f64 = 100.0;

/// Cached 256-point Hann window over the 160 PCM samples (zero past
/// `TONE_DETECT_FRAME`). Built once on first call.
fn hann_window() -> &'static [f64; FFT_SIZE] {
    use std::sync::OnceLock;
    static WINDOW: OnceLock<[f64; FFT_SIZE]> = OnceLock::new();
    WINDOW.get_or_init(|| {
        let two_pi = 2.0 * core::f64::consts::PI;
        let mut w = [0.0f64; FFT_SIZE];
        for n in 0..TONE_DETECT_FRAME {
            w[n] = 0.5 - 0.5 * (two_pi * n as f64 / TONE_DETECT_FRAME as f64).cos();
        }
        w
    })
}

/// Compute the windowed PSD for one detection frame.
///
/// Returns `(psd, floor)` where `psd[k] = |X(k)|²` for `k ∈ [0, 128]`
/// and `floor` is the 25th-percentile bin power (broadband baseline).
/// Hann-windowed, 256-point zero-padded FFT.
fn compute_psd(pcm: &[i16; TONE_DETECT_FRAME]) -> ([f64; FFT_SIZE / 2 + 1], f64) {
    use std::sync::OnceLock;
    use num_complex::Complex;
    use rustfft::{Fft, FftPlanner};

    static FFT: OnceLock<std::sync::Arc<dyn Fft<f64>>> = OnceLock::new();
    let fft = FFT.get_or_init(|| {
        let mut planner = FftPlanner::<f64>::new();
        planner.plan_fft_forward(FFT_SIZE)
    });

    let window = hann_window();
    let mut buf = [Complex::<f64>::new(0.0, 0.0); FFT_SIZE];
    for n in 0..TONE_DETECT_FRAME {
        buf[n] = Complex::new(pcm[n] as f64 * window[n], 0.0);
    }
    fft.process(&mut buf);

    let mut psd = [0.0f64; FFT_SIZE / 2 + 1];
    for k in 0..=FFT_SIZE / 2 {
        psd[k] = buf[k].re * buf[k].re + buf[k].im * buf[k].im;
    }

    // 25th-percentile broadband floor, excluding DC. `select_nth_unstable`
    // is O(N) and partitions in place — no full sort, no Vec alloc.
    let mut samples = [0.0f64; FFT_SIZE / 2];
    samples.copy_from_slice(&psd[1..]);
    let idx = samples.len() / 4;
    samples.select_nth_unstable_by(idx, |a, b| {
        a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
    });
    let floor = samples[idx];
    (psd, floor)
}

/// Quadratic peak interpolation for sub-bin frequency accuracy.
fn interpolate_peak_hz(psd: &[f64], k: usize) -> f64 {
    if k > 0 && k + 1 < psd.len() {
        let y_m1 = psd[k - 1].sqrt();
        let y_0 = psd[k].sqrt();
        let y_p1 = psd[k + 1].sqrt();
        let denom = y_m1 - 2.0 * y_0 + y_p1;
        let delta = if denom.abs() > 1e-12 {
            0.5 * (y_m1 - y_p1) / denom
        } else {
            0.0
        };
        (k as f64 + delta) * SAMPLE_RATE_HZ / FFT_SIZE as f64
    } else {
        k as f64 * SAMPLE_RATE_HZ / FFT_SIZE as f64
    }
}

/// Reference FFT magnitude for `A_D = 127` — set so a full-scale sine
/// (PCM peak 32767) round-trips at unity. Derivation:
///
/// For an exact-bin sine of PCM peak `A` through a Hann window of
/// length `N=160` (`Σw[n] = N/2 = 80`), the peak FFT magnitude is
/// `A · Σw / 2 = A · 40`. Full-scale `A = 32767` ⇒ peak ≈ 1 310 680.
///
/// On the synth side, voiced harmonics add `2·M̃_l` to the time-domain
/// output (`scale = 2.0 * m_curr_l` in the UvV branch); tone frames are
/// all-voiced so `γ_w` does not enter the path. Setting
/// `REF_PEAK_MAG = 1_310_000` makes the encoder emit `A_D = 127` exactly
/// at full-scale input, where `M̃_l = 16384` and the synth output peak
/// reaches `2·16384 = 32768` (i16 saturation). Across 24 dB the
/// round-trip residual is within ±0.3 dB on the 32-amplitude / 4-frequency
/// dense probe in `examples/tone_amp_audit.rs`.
const REF_PEAK_MAG: f64 = 1_310_000.0;

/// dB per A_D step matching Annex T Eq. 209 exactly:
/// `M̃_l = 16384 · 10^{0.03555·(A_D − 127)}` →
/// `20·log10(10^0.03555) = 0.711 dB/step`. Using anything else creates
/// a slope mismatch between the encoder's amplitude estimator and the
/// decoder's amplitude reconstruction (e.g., 0.5 dB/step would
/// expand round-trip error by 6 dB per 12 A_D steps).
const DB_PER_AD_STEP: f64 = 0.7115;

/// Convert a peak magnitude to the 7-bit `A_D` field. Inverse of Eq.
/// 209: `A_D = 127 + 20·log10(mag / REF_PEAK_MAG) / DB_PER_AD_STEP`.
fn magnitude_to_amplitude(mag: f64) -> u8 {
    let amp_db = 20.0 * (mag / REF_PEAK_MAG).log10();
    (127.0 + amp_db / DB_PER_AD_STEP).round().clamp(0.0, 127.0) as u8
}

/// Detect a single-frequency Annex T tone in `pcm`. Returns the
/// matching `(I_D, A_D)` or `None` if the frame doesn't look like a
/// tone (no dominant peak, peak-frequency mismatch, peak too weak
/// vs the spectral floor).
///
/// `pcm.len()` must equal [`TONE_DETECT_FRAME`].
pub fn detect_single_tone(pcm: &[i16]) -> Option<ToneDetection> {
    if pcm.len() != TONE_DETECT_FRAME {
        return None;
    }
    let pcm: &[i16; TONE_DETECT_FRAME] = pcm.try_into().ok()?;
    let (psd, floor) = compute_psd(pcm);

    // Find argmax peak excluding DC bin.
    let mut peak_k = 1usize;
    let mut peak_p = psd[1];
    for (k, &p) in psd.iter().enumerate().skip(2) {
        if p > peak_p {
            peak_p = p;
            peak_k = k;
        }
    }
    if floor <= 0.0 || peak_p / floor < SNR_THRESHOLD {
        return None;
    }
    let f_hz = interpolate_peak_hz(&psd, peak_k);

    // Match against single-tone Annex T rows (l1 == l2). The audible tone
    // is at `l1·f0`; rows with l1>1 cover frequencies above the l=1 band's
    // 156-375 Hz range (e.g. l1=l2=3 with f0≈343.76 ⇒ 1031 Hz, the
    // Motorola standard tone test pattern). Multi-tone rows (l1!=l2)
    // belong to detect_dtmf.
    let mut best: Option<(usize, f64)> = None;
    for (id, entry) in ANNEX_T.iter().enumerate() {
        let Some(t) = entry else { continue };
        if t.l1 != t.l2 {
            continue;
        }
        let expected_hz = f64::from(t.l1) * f64::from(t.f0);
        let diff = (f_hz - expected_hz).abs();
        if diff < MATCH_TOLERANCE_HZ
            && best.map_or(true, |(_, prev)| diff < prev)
        {
            best = Some((id, diff));
        }
    }
    let id = best?.0 as u8;
    Some(ToneDetection { id, amplitude: magnitude_to_amplitude(peak_p.sqrt()) })
}

/// Detect a DTMF (two-tone) Annex T entry in `pcm`. Returns the
/// matching `(I_D, A_D)` or `None` on no clean two-peak match.
///
/// DTMF rows in Annex T have `l1 != l2`; the audible tones are at
/// `l1·f0` and `l2·f0`. Detection finds the two strongest spectral
/// peaks (separated by ≥3 bins to avoid main-lobe siblings), then
/// scans Annex T DTMF rows for the one whose `(l1·f0, l2·f0)` pair
/// best matches the detected pair.
///
/// `pcm.len()` must equal [`TONE_DETECT_FRAME`].
pub fn detect_dtmf(pcm: &[i16]) -> Option<ToneDetection> {
    if pcm.len() != TONE_DETECT_FRAME {
        return None;
    }
    let pcm: &[i16; TONE_DETECT_FRAME] = pcm.try_into().ok()?;
    let (psd, floor) = compute_psd(pcm);

    // Find primary peak.
    let mut peak1_k = 1usize;
    let mut peak1_p = psd[1];
    for (k, &p) in psd.iter().enumerate().skip(2) {
        if p > peak1_p {
            peak1_p = p;
            peak1_k = k;
        }
    }
    if floor <= 0.0 || peak1_p / floor < SNR_THRESHOLD {
        return None;
    }

    // Find secondary peak: best k >= 2 bins away from primary, also
    // above floor.
    let mut peak2_k: Option<usize> = None;
    let mut peak2_p = floor; // must beat floor
    for (k, &p) in psd.iter().enumerate().skip(1) {
        if k.abs_diff(peak1_k) < 3 {
            continue;
        }
        if p > peak2_p {
            peak2_p = p;
            peak2_k = Some(k);
        }
    }
    let peak2_k = peak2_k?;
    // Secondary peak must clear SNR floor on its own.
    if peak2_p / floor < SNR_THRESHOLD {
        return None;
    }

    let f1_hz = interpolate_peak_hz(&psd, peak1_k);
    let f2_hz = interpolate_peak_hz(&psd, peak2_k);
    let f_lo = f1_hz.min(f2_hz);
    let f_hi = f1_hz.max(f2_hz);

    // Scan DTMF rows in Annex T for the closest (l1·f0, l2·f0) match.
    // Sort l1, l2 so we always compare against `(l_lo·f0, l_hi·f0)`
    // with l_lo < l_hi.
    let mut best: Option<(usize, f64)> = None;
    for (id, entry) in ANNEX_T.iter().enumerate() {
        let Some(t) = entry else { continue };
        if t.l1 == t.l2 {
            continue; // single-tone, handled by detect_single_tone
        }
        let f0 = f64::from(t.f0);
        let l_lo = t.l1.min(t.l2) as f64;
        let l_hi = t.l1.max(t.l2) as f64;
        let expected_lo = l_lo * f0;
        let expected_hi = l_hi * f0;
        let err = (expected_lo - f_lo).abs() + (expected_hi - f_hi).abs();
        if err < 2.0 * MATCH_TOLERANCE_HZ
            && best.map_or(true, |(_, prev)| err < prev)
        {
            best = Some((id, err));
        }
    }
    let id = best?.0 as u8;
    // Use the larger of the two peak magnitudes for amplitude (DTMF
    // tones are typically equal-amplitude; a one-off pick is fine).
    let mag = peak1_p.sqrt().max(peak2_p.sqrt());
    Some(ToneDetection { id, amplitude: magnitude_to_amplitude(mag) })
}

/// Try DTMF detection first, then single-tone. Returns whichever
/// matches; `None` if neither does.
///
/// DTMF is more constraining (two peaks at specific harmonic ratios)
/// so a hit is high-confidence; falling through to single-tone after
/// DTMF fails handles the common case cleanly.
pub fn detect_tone(pcm: &[i16]) -> Option<ToneDetection> {
    detect_dtmf(pcm).or_else(|| detect_single_tone(pcm))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn pure_sine(freq_hz: f64, amplitude: i16) -> [i16; TONE_DETECT_FRAME] {
        let mut out = [0i16; TONE_DETECT_FRAME];
        let two_pi = 2.0 * core::f64::consts::PI;
        for (n, slot) in out.iter_mut().enumerate() {
            let s = (amplitude as f64) * (two_pi * freq_hz * n as f64 / SAMPLE_RATE_HZ).sin();
            *slot = s.round() as i16;
        }
        out
    }

    fn dtmf_pair(f1: f64, f2: f64, amp_each: i16) -> [i16; TONE_DETECT_FRAME] {
        let mut out = [0i16; TONE_DETECT_FRAME];
        let two_pi = 2.0 * core::f64::consts::PI;
        for (n, slot) in out.iter_mut().enumerate() {
            let s1 = (amp_each as f64) * (two_pi * f1 * n as f64 / SAMPLE_RATE_HZ).sin();
            let s2 = (amp_each as f64) * (two_pi * f2 * n as f64 / SAMPLE_RATE_HZ).sin();
            *slot = (s1 + s2).round().clamp(-32768.0, 32767.0) as i16;
        }
        out
    }

    #[test]
    fn detects_clean_sine_at_annex_t_frequency() {
        // Annex T entry id=10 → f0=312.5 Hz (single-tone).
        let pcm = pure_sine(312.5, 8000);
        let det = detect_single_tone(&pcm).expect("clean tone should be detected");
        assert_eq!(det.id, 10);
        assert!(det.amplitude > 50, "amplitude clamped to >0: {}", det.amplitude);
    }

    #[test]
    fn rejects_white_noise() {
        // Pseudo-noise (LCG-driven) has no clean peak → no tone.
        let mut pcm = [0i16; TONE_DETECT_FRAME];
        let mut state: u32 = 12345;
        for slot in pcm.iter_mut() {
            state = state.wrapping_mul(1664525).wrapping_add(1013904223);
            *slot = ((state as i32 >> 16) as i16) / 4;
        }
        assert!(detect_single_tone(&pcm).is_none());
    }

    #[test]
    fn rejects_silence() {
        let pcm = [0i16; TONE_DETECT_FRAME];
        assert!(detect_single_tone(&pcm).is_none());
    }

    #[test]
    fn snr_floor_rejects_weak_tone_buried_in_noise() {
        // Tone at -30 dBFS + noise at -20 dBFS.
        let mut pcm = pure_sine(312.5, 1000);
        let mut state: u32 = 7777;
        for slot in pcm.iter_mut() {
            state = state.wrapping_mul(1664525).wrapping_add(1013904223);
            let n = ((state as i32 >> 16) as i16) / 4;
            *slot = slot.saturating_add(n);
        }
        // Tone may or may not be detected — depends on the SNR threshold.
        // Test that whichever path it takes, it doesn't panic.
        let _ = detect_single_tone(&pcm);
    }

    #[test]
    fn wrong_length_returns_none() {
        let pcm = [0i16; 100];
        assert!(detect_single_tone(&pcm).is_none());
    }

    #[test]
    fn tolerance_window_snaps_to_nearest_annex_entry() {
        // Drift slightly off id=10's 312.5 Hz — should still match.
        let pcm = pure_sine(316.0, 8000);
        let det = detect_single_tone(&pcm).expect("near-match should detect");
        assert_eq!(det.id, 10);
    }

    #[test]
    fn detects_standard_motorola_test_tone_at_l3_harmonic() {
        // Motorola's Standard Tone Test Pattern is 1031.25 Hz IMBE-encoded.
        // Annex T has a row at l1=l2=3, f0≈343.76 Hz where the audible tone
        // is 3·f0 ≈ 1031.28 Hz — the natural target for half-rate Annex T
        // dispatch on this conformance signal. Regression for the pre-fix
        // bug where detect_single_tone restricted matches to l1=1 entries
        // and silently dropped this whole class of tones.
        let pcm = pure_sine(1031.25, 8000);
        let det = detect_single_tone(&pcm).expect("1031 Hz tone should match an l=3 row");
        let entry = ANNEX_T[det.id as usize].expect("matched id has Annex T entry");
        assert_eq!(entry.l1, entry.l2, "single-tone row");
        assert_eq!(entry.l1, 3, "1031 Hz is in the l=3 band of Annex T");
        let expected_hz = f64::from(entry.l1) * f64::from(entry.f0);
        assert!(
            (expected_hz - 1031.25).abs() < f64::from(MATCH_TOLERANCE_HZ as f32),
            "l·f0 ({expected_hz:.2}) should be within tolerance of 1031.25"
        );
    }

    #[test]
    fn detect_dtmf_matches_known_annex_t_pair() {
        // Annex T id=128 → f0=78.5, l1=12, l2=17 → tones at 942 Hz
        // and 1334.5 Hz (close to DTMF "1" 941/1336).
        let pcm = dtmf_pair(942.0, 1334.5, 6000);
        let det = detect_dtmf(&pcm).expect("clean DTMF should be detected");
        assert_eq!(det.id, 128);
    }

    #[test]
    fn detect_tone_prefers_dtmf_when_two_peaks_present() {
        // Same DTMF input — `detect_tone` should route through DTMF
        // rather than picking up either peak as a single-tone match.
        let pcm = dtmf_pair(942.0, 1334.5, 6000);
        let det = detect_tone(&pcm).expect("tone-detect dispatch hit");
        assert_eq!(det.id, 128);
    }

    #[test]
    fn detect_dtmf_rejects_single_tone_input() {
        // A pure sine has only one peak; secondary-peak SNR check
        // should reject as not-DTMF.
        let pcm = pure_sine(312.5, 8000);
        assert!(detect_dtmf(&pcm).is_none());
        // But detect_tone() falls through to single-tone and finds it.
        let det = detect_tone(&pcm).expect("falls through to single-tone");
        assert_eq!(det.id, 10);
    }

    #[test]
    fn detect_dtmf_rejects_noise() {
        let mut pcm = [0i16; TONE_DETECT_FRAME];
        let mut state: u32 = 9999;
        for slot in pcm.iter_mut() {
            state = state.wrapping_mul(1664525).wrapping_add(1013904223);
            *slot = ((state as i32 >> 16) as i16) / 4;
        }
        assert!(detect_dtmf(&pcm).is_none());
    }

    /// Knox tone-pairs occupy Annex T IDs 144–159 (see analysis memo
    /// `reference_tone_frames_phase1_vs_phase2_2026-04-30.md`). For each
    /// in-range Knox ID, synthesize the matching `(l1·f0, l2·f0)` pair
    /// and verify `detect_dtmf` returns that exact ID. Validates the
    /// matcher against the full Knox sub-table at once — a regression
    /// here would mean a mis-classification shipping into the
    /// tone-frame dispatch path on real Knox traffic.
    #[test]
    fn detect_dtmf_round_trips_each_knox_id() {
        // Skip IDs whose harmonic ratio places `l_lo·f0` below ~150 Hz
        // (the lower edge of the Annex T frequency space) — those have
        // a very low primary peak that gets close to DC and risks
        // colliding with the DC-skip rule in `compute_psd`. None of
        // the Knox IDs hit this in practice, but the guard makes the
        // test robust against future table additions.
        for knox_id in 144u8..=159 {
            let entry = ANNEX_T[knox_id as usize].expect("Knox ID has Annex T row");
            assert_ne!(entry.l1, entry.l2, "Knox IDs are pairs, l1 != l2");
            let f0 = f64::from(entry.f0);
            let f_lo = f0 * f64::from(entry.l1.min(entry.l2));
            let f_hi = f0 * f64::from(entry.l1.max(entry.l2));
            // Skip if either tone falls outside the FFT-resolvable band.
            if f_lo < 150.0 || f_hi > 3500.0 {
                continue;
            }
            let pcm = dtmf_pair(f_lo, f_hi, 6000);
            let det = detect_dtmf(&pcm)
                .unwrap_or_else(|| panic!("Knox ID {knox_id} should detect"));
            assert_eq!(
                det.id, knox_id,
                "Knox ID {knox_id} (f_lo={f_lo:.1} Hz, f_hi={f_hi:.1} Hz) misdetected as {}",
                det.id
            );
        }
    }

    /// `detect_tone` (the dispatch entry point) must route each Knox
    /// pair through the DTMF matcher rather than falling through to
    /// `detect_single_tone` (which would pick whichever single-tone
    /// row sits closest to the louder peak).
    #[test]
    fn detect_tone_routes_knox_pairs_through_dtmf_matcher() {
        // Spot-check id=145 (Knox A+B 603.6/1056.2 Hz — the actual
        // pair in DVSI's knox_1.pcm test vector).
        let entry = ANNEX_T[145].unwrap();
        let f_lo = f64::from(entry.f0) * f64::from(entry.l1.min(entry.l2));
        let f_hi = f64::from(entry.f0) * f64::from(entry.l1.max(entry.l2));
        let pcm = dtmf_pair(f_lo, f_hi, 6000);
        let det = detect_tone(&pcm).expect("Knox pair detect");
        assert_eq!(det.id, 145);
    }
}