Skip to main content

blip25_mbe/
vocoder.rs

1//! Unified [`Vocoder`] handle — chip-shaped façade over the rate-specific
2//! pipelines.
3//!
4//! The DVSI AMBE-3000R exposes a single per-channel handle with uniform
5//! `encode` / `decode` / `reset` operations regardless of the configured
6//! rate. This module reproduces that surface for in-process Rust callers:
7//! one [`Vocoder`] handle owns all rate-specific state (analysis,
8//! decoder, synth) and dispatches uniformly across rates selected at
9//! runtime via the [`Rate`] enum.
10//!
11//! The low-level modules ([`crate::imbe_wire`], [`crate::ambe_plus2_wire`],
12//! [`crate::codecs::mbe_baseline`]) stay public for advanced consumers
13//! that need to drive the pipeline frame-by-frame at the parameter
14//! layer; this module is the recommended entry point for everything else.
15//!
16//! ## Quick start
17//!
18//! ```rust
19//! use blip25_mbe::vocoder::{Rate, Vocoder};
20//!
21//! // P25 Phase 1 (full-rate IMBE) encoder
22//! let mut tx = Vocoder::new(Rate::Imbe7200x4400);
23//! let pcm: [i16; 160] = [0; 160]; // one 20 ms frame at 8 kHz
24//! let bits = tx.encode_pcm(&pcm).expect("encode");
25//! assert_eq!(bits.len(), 18); // 18-byte FEC frame
26//!
27//! // P25 Phase 1 decoder (separate channel, separate state)
28//! let mut rx = Vocoder::new(Rate::Imbe7200x4400);
29//! let pcm = rx.decode_bits(&bits).expect("decode");
30//! assert_eq!(pcm.len(), 160);
31//! ```
32//!
33//! ## Mapping to the chip protocol
34//!
35//! | Chip operation                | This module                          |
36//! |-------------------------------|---------------------------------------|
37//! | Open channel                  | [`Vocoder::new`]                     |
38//! | Set rate (`PKT_RATEP`)        | [`Rate`] argument at construction    |
39//! | Reset (re-send `PKT_RATEP`)   | [`Vocoder::reset`]                   |
40//! | Encode 160-sample PCM → bits  | [`Vocoder::encode_pcm`]              |
41//! | Decode bits → 160-sample PCM  | [`Vocoder::decode_bits`]             |
42//! | Read last-frame stats         | [`Vocoder::last_stats`]              |
43//! | Frame size                    | [`Vocoder::frame_samples`] / [`Vocoder::fec_frame_bytes`] |
44//!
45//! Rate is fixed for the lifetime of a [`Vocoder`]; build a new handle
46//! to switch rates (mirrors a chip's PKT_RATEP cycle).
47
48use crate::codecs::ambe_plus2;
49use crate::codecs::mbe_baseline::analysis::{
50    AnalysisError, AnalysisOutput, AnalysisState, ToneDetection,
51    detect_tone, encode as analysis_encode,
52    encode_ambe_plus2 as analysis_encode_ambe_plus2,
53    profile as analysis_profile,
54};
55use crate::codecs::mbe_baseline::{
56    FrameDisposition, FrameErrorContext, GAMMA_W, SynthState, UnvoicedNoiseGen, synthesize_frame,
57};
58use crate::enhancement::{self, EnhancementMode, EnhancementState};
59use crate::mbe_params::MbeParams;
60use crate::imbe_wire;
61use crate::ambe_plus2_wire;
62
63/// 8 kHz mono — the only sample rate the vocoder produces.
64const SAMPLE_RATE_HZ: f32 = 8_000.0;
65
66/// Number of i16 PCM samples per 20 ms frame at 8 kHz. Constant across
67/// every supported rate.
68pub const FRAME_SAMPLES: usize = 160;
69
70/// Vocoder rate selection — picks both the codec generation and the
71/// wire-FEC framing.
72///
73/// More variants will land as additional carriers are added. The
74/// values themselves are also stable Wire-format choices: a [`Rate`]
75/// is enough to know how many FEC bytes a frame is, what codec
76/// generation drives the synth, and which dequantize tables to use.
77#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79#[non_exhaustive]
80pub enum Rate {
81    /// P25 Phase 1 FDMA full-rate IMBE. 18-byte FEC frame (72 dibits).
82    /// 7 200 bps total / 4 400 bps voice + 2 800 bps FEC.
83    Imbe7200x4400,
84    /// IMBE info-only — same codec as [`Self::Imbe7200x4400`] with the
85    /// Annex H Golay/Hamming/PN FEC layer stripped. 11-byte wire frame
86    /// (88 prioritized info bits packed MSB-first). 4 400 bps total
87    /// (= voice). Byte layout matches JMBE / OP25 / DVSI `p25_nofec`
88    /// convention bit-for-bit (validated against DVSI tv-rc references
89    /// at 100% bit-exact). Use this for storage when you specifically
90    /// need an info-only IMBE archive; otherwise prefer the
91    /// FEC-bearing [`Self::Imbe7200x4400`] format — see
92    /// `docs/wire_formats_and_storage.md`.
93    Imbe4400x4400,
94    /// P25 Phase 2 TDMA half-rate AMBE+2. 9-byte FEC frame (36 dibits).
95    /// 3 600 bps total / 2 450 bps voice + 1 150 bps FEC. Also the
96    /// vocoder-layer format for DMR Tier II/III voice frames (modulo
97    /// carrier-specific burst framing).
98    AmbePlus2_3600x2450,
99    /// AMBE+2 half-rate info-only — same 49 info bits as
100    /// [`Self::AmbePlus2_3600x2450`] with the Golay/Hamming/PN FEC
101    /// layer stripped. 7-byte wire frame (49 info bits packed
102    /// MSB-first in u₀..u₃ order, 7 trailing pad bits). 2 450 bps
103    /// total (= voice).
104    ///
105    /// **Byte layout caveat.** DVSI's chip rate-index 34 emits the
106    /// same 49 info bits in a *different* (chip-internal, permuted)
107    /// byte order. This crate's layout is self-consistent (ours-to-
108    /// ours round-trip is lossless) but is not byte-equal to DVSI's
109    /// r34 stream. If you need byte-exact DVSI r34 interop, see the
110    /// future `blip25-chip-shim` crate; here this variant is for
111    /// compact ours-to-ours archival. Recommended storage format is
112    /// the FEC-bearing [`Self::AmbePlus2_3600x2450`] — see
113    /// `docs/wire_formats_and_storage.md`.
114    AmbePlus2_2450x2450,
115}
116
117impl Rate {
118    /// Number of bytes in one wire frame at this rate. Includes FEC
119    /// for the FEC-bearing variants ([`Self::Imbe7200x4400`],
120    /// [`Self::AmbePlus2_3600x2450`]) and is just the packed info bits
121    /// for the no-FEC variants ([`Self::Imbe4400x4400`],
122    /// [`Self::AmbePlus2_2450x2450`]).
123    #[inline]
124    pub const fn fec_frame_bytes(self) -> usize {
125        match self {
126            Rate::Imbe7200x4400 => 18,
127            Rate::Imbe4400x4400 => 11,
128            Rate::AmbePlus2_3600x2450 => 9,
129            Rate::AmbePlus2_2450x2450 => 7,
130        }
131    }
132
133    /// PCM samples per frame (always 160 at 8 kHz / 20 ms; provided
134    /// for symmetry with [`Self::fec_frame_bytes`]).
135    #[inline]
136    pub const fn frame_samples(self) -> usize {
137        FRAME_SAMPLES
138    }
139}
140
141/// Half-rate synthesis generation. Picks how reconstructed `MbeParams`
142/// are turned back into PCM on the half-rate decode path.
143///
144/// Full-rate (P25 Phase 1 IMBE) ignores this — it always uses the
145/// BABA-A §1.12 baseline phase, the only synth the IMBE spec
146/// describes.
147///
148/// The original Wave 1.2 of `SCOPE_PLAN.md` framed this as an
149/// "AMBE+ Gen-2 dequantize wrapper" — that turned out to be the
150/// wrong axis. The half-rate WIRE format and parameter recovery are
151/// AMBE+2 across the board (per
152/// `~/blip25-specs/analysis/oss_implementations_lessons_learned.md`);
153/// the actual Gen-2 vs Gen-3 split for legacy NXDN / DMR is here, on
154/// the synth side. mbelib's half-rate decoder uses [`Self::Baseline`]
155/// (1993-vintage IMBE phase, no Hilbert phase regen). JMBE / SDRTrunk
156/// use [`Self::AmbePlus`] (US5701390 phase regen, modern AMBE+ /
157/// AMBE+2 sound).
158#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160#[non_exhaustive]
161pub enum AmbePlus2Synth {
162    /// US5701390 phase regen — modern AMBE+ / AMBE+2 sound. Default
163    /// and recommended for P25 Phase 2 / NXDN type-2 / DMR enhanced.
164    AmbePlus,
165    /// BABA-A §1.12 baseline IMBE phase — no Hilbert regen. Matches
166    /// mbelib's half-rate output. Use for legacy NXDN type-1 / older
167    /// DMR captures where the consumer wants mbelib-equivalent
168    /// audible behavior.
169    Baseline,
170}
171
172impl Default for AmbePlus2Synth {
173    fn default() -> Self {
174        Self::AmbePlus
175    }
176}
177
178/// Per-frame statistics recorded by the most recent [`Vocoder::encode_pcm`]
179/// or [`Vocoder::decode_bits`] call. `None` until at least one frame
180/// has been processed.
181///
182/// Encode-side fills only `analysis`; decode-side fills only `decode`.
183#[derive(Clone, Debug, Default)]
184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
185pub struct FrameStats {
186    /// Stats from the last encoded frame.
187    pub analysis: Option<AnalysisStats>,
188    /// Stats from the last decoded frame.
189    pub decode: Option<DecodeStats>,
190}
191
192/// Encode-side per-frame stats.
193#[derive(Clone, Debug)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195pub struct AnalysisStats {
196    /// What the analysis encoder emitted (`Voice` / `Silence` / `Tone`).
197    pub output: AnalysisOutputKind,
198    /// The MBE parameters that were quantized into the wire bits.
199    /// Populated for both `Voice` and `Silence` (silence dispatches
200    /// the rate-appropriate placeholder).
201    pub params: MbeParams,
202    /// Tone-detection signal for the input frame, populated whenever
203    /// [`Vocoder::set_tone_detection`] is enabled and the detector
204    /// matched an Annex T entry. Decoupled from `output` because Phase
205    /// 1 IMBE has no tone-frame opcode (per BABA-A): on full-rate the
206    /// detector still surfaces `(I_D, A_D)` here for application-layer
207    /// signaling (LCW, paging, dispatch) while the wire frame remains
208    /// a regular voice / silence frame. On half-rate AMBE+2 a positive
209    /// detection is *also* dispatched as an Annex T tone frame (so
210    /// `output == Tone` and this field both populate).
211    pub tone_detect: Option<ToneDetection>,
212}
213
214/// Discriminator-only counterpart of `AnalysisOutput` — strips the
215/// `MbeParams` payload so consumers can inspect what kind of frame
216/// was emitted without holding a copy. The full params are in
217/// [`AnalysisStats::params`].
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
219#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
220pub enum AnalysisOutputKind {
221    /// Real voice frame derived from the input PCM.
222    Voice,
223    /// Silence-dispatched frame (rate-appropriate placeholder params).
224    Silence,
225    /// Annex T tone frame — encode-side detected a clean tone in the
226    /// PCM and emitted the matching `(I_D, A_D)` payload instead of
227    /// running the voice analysis pipeline. Half-rate only; gated on
228    /// [`Vocoder::set_tone_detection`]. Default off.
229    Tone {
230        /// Annex T tone ID.
231        id: u8,
232        /// 7-bit log-amplitude (`A_D`).
233        amplitude: u8,
234    },
235}
236
237/// Decode-side per-frame stats.
238#[derive(Clone, Debug)]
239#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
240pub struct DecodeStats {
241    /// FEC error count on the leading codeword (Golay c̃₀ for full-rate,
242    /// AMBE+2 c̃₀ for half-rate).
243    pub epsilon_0: u8,
244    /// Total FEC error count across all coded vectors of the frame.
245    pub epsilon_t: u8,
246    /// Synth-side disposition for this frame (Use / Repeat / Mute).
247    /// Populated for both rates after the synth runs. `None` only on
248    /// frames where dequantize errored before synth was reached
249    /// (e.g. invalid pitch index in full-rate; the field then carries
250    /// over the prior frame's value via `Vocoder::synth.last_disposition`).
251    pub disposition: Option<FrameDisposition>,
252}
253
254/// Errors that can surface from [`Vocoder`] operations. Wraps the
255/// rate-specific error types so a single `?` covers any Vocoder call.
256#[derive(Debug)]
257pub enum VocoderError {
258    /// Input PCM slice was the wrong length. Always `frame_samples()`.
259    WrongPcmLength {
260        /// Number of samples the channel expected.
261        expected: usize,
262        /// Number of samples the caller passed.
263        got: usize,
264    },
265    /// Input bit slice was the wrong length. Always `fec_frame_bytes()`.
266    WrongBitsLength {
267        /// Number of bytes the channel expected.
268        expected: usize,
269        /// Number of bytes the caller passed.
270        got: usize,
271    },
272    /// Analysis encoder failure (HPF / pitch / refinement).
273    Analysis(AnalysisError),
274    /// Wire-quantize failure (e.g. predictor returned a non-finite value).
275    Quantize(String),
276    /// Requested transcode direction is not supported. The pair of rates
277    /// has no parameter-domain converter wired up.
278    UnsupportedTranscode {
279        /// Rate of the input FEC frame.
280        from: Rate,
281        /// Rate of the output FEC frame.
282        to: Rate,
283    },
284}
285
286impl core::fmt::Display for VocoderError {
287    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
288        match self {
289            VocoderError::WrongPcmLength { expected, got } => {
290                write!(f, "expected {expected} PCM samples per frame, got {got}")
291            }
292            VocoderError::WrongBitsLength { expected, got } => {
293                write!(f, "expected {expected} FEC bytes per frame, got {got}")
294            }
295            VocoderError::Analysis(e) => write!(f, "analysis encoder error: {e:?}"),
296            VocoderError::Quantize(msg) => write!(f, "quantize error: {msg}"),
297            VocoderError::UnsupportedTranscode { from, to } => {
298                write!(f, "unsupported transcode direction: {from:?} -> {to:?}")
299            }
300        }
301    }
302}
303
304impl std::error::Error for VocoderError {}
305
306/// Chip-shaped façade over the rate-specific encoder + decoder + synth
307/// pipelines.
308///
309/// Owns all per-rate state internally (analysis, decoder, synth). One
310/// [`Vocoder`] is *both* encoder and decoder for a single channel
311/// direction; consumers running bidirectional voice typically allocate
312/// two — one each direction — to mirror the chip's per-direction
313/// state isolation.
314///
315/// State is not `Sync`; one channel = one thread. State is `Send`,
316/// so the channel can move between threads.
317pub struct Vocoder {
318    rate: Rate,
319    analysis: AnalysisState,
320    imbe_dec: imbe_wire::dequantize::DecoderState,
321    ambe_plus2_dec: ambe_plus2_wire::dequantize::DecoderState,
322    synth: SynthState,
323    last_stats: FrameStats,
324    tone_detection: bool,
325    ambe_plus2_synth: AmbePlus2Synth,
326    enhancement: EnhancementMode,
327    enhancement_state: EnhancementState,
328    prev_disposition: Option<FrameDisposition>,
329}
330
331impl Vocoder {
332    /// Open a new channel at the given rate, all state cold.
333    ///
334    /// The unvoiced noise generator is selected per-rate: IMBE full-rate
335    /// uses the BABA-A §1.12.1 spec LCG (171/11213/53125, seed 3147)
336    /// because the DVSI chip's full-rate path matches that recurrence;
337    /// AMBE+2 half-rate uses the chip-empirical LCG (173/13849/65536,
338    /// seed 60584) — see gap report 0025. Probe 1 confirmed 0.945
339    /// sample-level correlation against the live chip on the half-rate
340    /// path.
341    pub fn new(rate: Rate) -> Self {
342        let noise_gen = match rate {
343            Rate::Imbe7200x4400 | Rate::Imbe4400x4400 => UnvoicedNoiseGen::SpecLcg,
344            Rate::AmbePlus2_3600x2450 | Rate::AmbePlus2_2450x2450 => UnvoicedNoiseGen::ChipLcg,
345        };
346        Self {
347            rate,
348            analysis: AnalysisState::new(),
349            imbe_dec: imbe_wire::dequantize::DecoderState::new(),
350            ambe_plus2_dec: ambe_plus2_wire::dequantize::DecoderState::new(),
351            synth: SynthState::with_unvoiced_gen(noise_gen),
352            last_stats: FrameStats::default(),
353            tone_detection: false,
354            ambe_plus2_synth: AmbePlus2Synth::AmbePlus,
355            // Enhancement: Classical-by-default since 2026-05-14. The
356            // HPF + presence + boundary-fade chain in
357            // `ClassicalConfig::default()` carries a free +0.03 to
358            // +0.09 PESQ across the 5-vector matrix; spec-faithful
359            // PCM is one `.set_enhancement(EnhancementMode::None)`
360            // call away.
361            enhancement: EnhancementMode::Classical(crate::enhancement::ClassicalConfig::default()),
362            enhancement_state: EnhancementState::default(),
363            prev_disposition: None,
364        }
365    }
366
367    /// Configure which half-rate synth flavor [`Self::decode_bits`] +
368    /// [`Self::synthesize_params`] use on Phase 2 / AMBE+2 input.
369    /// No-op for full-rate (which always uses the BABA-A §1.12
370    /// baseline IMBE synth). Default [`AmbePlus2Synth::AmbePlus`].
371    pub fn set_ambe_plus2_synth(&mut self, gen: AmbePlus2Synth) {
372        self.ambe_plus2_synth = gen;
373    }
374
375    /// Currently configured half-rate synth flavor.
376    #[inline]
377    pub fn ambe_plus2_synth(&self) -> AmbePlus2Synth {
378        self.ambe_plus2_synth
379    }
380
381    /// Enable encode-side Annex T tone detection. Each PCM frame is
382    /// inspected for a clean single-frequency tone or DTMF / Knox
383    /// pair matching an Annex T entry; on a hit the detection result
384    /// is surfaced via [`AnalysisStats::tone_detect`]. Default off.
385    ///
386    /// Wire behavior is rate-dependent:
387    ///
388    /// - **Half-rate (AMBE+2 Phase 2):** on a hit, the channel emits
389    ///   an Annex T tone frame instead of running the voice analysis
390    ///   pipeline; [`AnalysisStats::output`] is `Tone { id, amplitude }`
391    ///   and [`AnalysisStats::tone_detect`] mirrors the same values.
392    /// - **Full-rate (IMBE Phase 1):** the wire frame remains a
393    ///   regular voice / silence frame because P25 Phase 1 has no
394    ///   tone-frame opcode at the codec layer (BABA-A; spec-author
395    ///   confirmation 2026-04-30). [`AnalysisStats::tone_detect`] is
396    ///   still populated so application-layer consumers can route
397    ///   `(I_D, A_D)` to LCW, paging, or other out-of-band signaling
398    ///   per BABA-A `§5.4`.
399    ///
400    /// Spec context: BABA-A `§2.10` defines the tone-frame *payload*
401    /// (Annex T table + signature bits) but the spec leaves "is this
402    /// a tone?" to the implementer (per `§0.0.1` of the impl spec).
403    /// This is a DSP design choice, not a P25-IP question.
404    pub fn set_tone_detection(&mut self, enabled: bool) {
405        self.tone_detection = enabled;
406    }
407
408    /// Whether encode-side tone detection is currently enabled.
409    #[inline]
410    pub fn tone_detection(&self) -> bool {
411        self.tone_detection
412    }
413
414    /// Configure the beyond-spec consecutive-repeat reset threshold
415    /// on the synth side. After `n` consecutive Repeat/Mute frames,
416    /// substitution falls back to a default-fundamental + amps=1.0
417    /// frame instead of replaying the prior `last_good`. `None`
418    /// (default) is the spec-faithful path per gap 0022 resolution.
419    /// JMBE / SDRTrunk use `Some(3)` for chip-stream interop quality.
420    pub fn set_repeat_reset_after(&mut self, n: Option<u32>) {
421        self.synth.set_repeat_reset_after(n);
422    }
423
424    /// Current consecutive-repeat reset threshold (`None` = disabled,
425    /// spec-faithful).
426    #[inline]
427    pub fn repeat_reset_after(&self) -> Option<u32> {
428        self.synth.repeat_reset_after()
429    }
430
431    /// Enable JMBE-style error-rate freeze on Repeat (beyond-spec; gap
432    /// 0021). When enabled, frames whose disposition is `Repeat` do
433    /// not advance `ε_R`; the previous frame's value is carried
434    /// forward. This mirrors JMBE's
435    /// `IMBEModelParameters.copy()` calling
436    /// `setErrorRate(previous.getErrorRate())` and prevents runs of
437    /// high-error chip-encoded frames from driving `ε_R` past the
438    /// 0.0875 Mute threshold. Default `false` is the spec-faithful
439    /// path. Enable for decoding chip-encoded P25 air traffic; leave
440    /// off for our-encoder → our-decoder loops and spec-conformance
441    /// testing.
442    pub fn set_chip_compat(&mut self, on: bool) {
443        self.synth.set_chip_compat(on);
444    }
445
446    /// Current chip-compat (error-rate freeze on Repeat) setting.
447    #[inline]
448    pub fn chip_compat(&self) -> bool {
449        self.synth.chip_compat()
450    }
451
452    /// Force the beyond-spec spectral-discontinuity clamp on (gap 0026)
453    /// independently of [`Self::set_chip_compat`]. Most consumers
454    /// should leave this off — the umbrella `chip_compat` flag already
455    /// auto-enables the clamp alongside the gap-0021 ε_R freeze.
456    /// This standalone toggle exists for the narrow case of wanting
457    /// the clamp without the ε_R freeze. With either flag enabled,
458    /// frames whose harmonic count `L` jumps by more than 5 from the
459    /// previous frame (up-jumps only) **or** whose `err.epsilon_0 ≥ 2`
460    /// have their post-smoothing amplitudes scaled by 0.73 for one
461    /// frame. Mirrors observed DVSI AMBE-3000R behavior on pitch/L
462    /// jumps and Repeat frames; reduces audible "scratch" on noisy
463    /// Phase 2 traffic where Golay-corrected frames decode to
464    /// spectrally-distant params from their neighbors. Default
465    /// `false` keeps the spec-faithful path.
466    pub fn set_chip_compat_spectral_clamp(&mut self, on: bool) {
467        self.synth.set_chip_compat_spectral_clamp(on);
468    }
469
470    /// Current standalone spectral-discontinuity clamp setting.
471    /// Note: the clamp also fires whenever [`Self::chip_compat`] is
472    /// `true`, regardless of this setting.
473    #[inline]
474    pub fn chip_compat_spectral_clamp(&self) -> bool {
475        self.synth.chip_compat_spectral_clamp()
476    }
477
478    /// Enable the §0.8.4 silence-dispatch path on the analysis encoder.
479    /// When on, frames whose energy clears the silence detector's
480    /// hysteresis emit `AnalysisOutput::Silence` (rate-appropriate
481    /// silence params) instead of running the full pitch / V-UV /
482    /// amplitude pipeline. Default off — pass-through per addendum
483    /// §0.8.8 recommendation.
484    pub fn set_silence_dispatch(&mut self, on: bool) {
485        self.analysis.set_silence_detection(on);
486    }
487
488    /// Whether silence dispatch is currently enabled.
489    #[inline]
490    pub fn silence_dispatch(&self) -> bool {
491        self.analysis.silence_detection_enabled()
492    }
493
494    /// Enable the joint-signal silence override — dispatches
495    /// pitch-unreliable low-energy frames as silence even if they
496    /// don't reach the §0.8.4 hysteresis threshold. Requires
497    /// [`Self::set_silence_dispatch`] also be on. Default off.
498    pub fn set_pitch_silence_override(&mut self, on: bool) {
499        self.analysis.set_pitch_silence_override(on);
500    }
501
502    /// Whether the joint-signal silence override is currently enabled.
503    #[inline]
504    pub fn pitch_silence_override(&self) -> bool {
505        self.analysis.pitch_silence_override_enabled()
506    }
507
508    /// Enable or disable the onset-attack mitigation. On near-silent
509    /// frames the encoder commits a short default pitch
510    /// (≈ 25 samples / ~320 Hz) to its look-back history instead of
511    /// the phantom long-period autocorrelation peak that quiet input
512    /// otherwise produces. The silent frame still encodes through the
513    /// full voice pipeline; only the next frame's `look_back`
514    /// continuity window is affected. Targets the 2-frame onset attack
515    /// at silence→loud transitions documented in
516    /// `project_onset_attack_2frame_2026-04-25.md`. Default off — eval
517    /// via the 5-vector PESQ harness before enabling in production.
518    pub fn set_default_pitch_on_silence(&mut self, on: bool) {
519        self.analysis.set_default_pitch_on_silence(on);
520    }
521
522    /// Whether the onset-attack mitigation is currently enabled.
523    #[inline]
524    pub fn default_pitch_on_silence(&self) -> bool {
525        self.analysis.default_pitch_on_silence_enabled()
526    }
527
528    /// Enable or disable the PYIN pitch frontend. When on, the analysis
529    /// encoder uses [`crate::codecs::mbe_baseline::analysis::run_pyin`]
530    /// for `(p_hat_i, e_p_hat_i)` instead of the §0.3 look-back /
531    /// look-ahead tracker (Eq. 5–23). PYIN is post-2002 DSP outside
532    /// BABA-A's clean-room scope; landed for A/B PESQ evaluation. The
533    /// §0.3 path remains the default and the spec-faithful baseline.
534    pub fn set_pyin_pitch(&mut self, on: bool) {
535        self.analysis.set_pyin_pitch(on);
536    }
537
538    /// Whether the PYIN pitch frontend is currently enabled.
539    #[inline]
540    pub fn pyin_pitch(&self) -> bool {
541        self.analysis.pyin_pitch_enabled()
542    }
543
544    /// Enable or disable input-side spectral subtraction (Boll 1979)
545    /// applied to `signal_spectrum` output before §0.5 amplitude
546    /// estimation. Per-bin running noise PSD is updated on
547    /// silence-flagged frames; voiced frames hold. Off by default —
548    /// targets noisy-tone inputs (memo
549    /// `project_amp_noise_sensitivity_2026-04-24`).
550    pub fn set_spectral_subtraction(&mut self, on: bool) {
551        self.analysis.set_spectral_subtraction(on);
552    }
553
554    /// Whether spectral subtraction is currently enabled.
555    #[inline]
556    pub fn spectral_subtraction(&self) -> bool {
557        self.analysis.spectral_subtraction_enabled()
558    }
559
560    /// Set the §0.5 amplitude EMA weight `α`. `0.0` disables the
561    /// smoother (default); `(0.0, 1.0]` enables
562    /// `M̂_l(t) = α · M̂_l + (1−α) · M̂_l(t−1)` per harmonic, gated by
563    /// pitch similarity. Targets the noisy-tone amp jitter described
564    /// in `project_amp_noise_sensitivity_2026-04-24`. Outside BABA-A
565    /// clean-room scope (general-DSP magnitude smoothing).
566    pub fn set_amp_ema_alpha(&mut self, alpha: f64) {
567        self.analysis.set_amp_ema_alpha(alpha);
568    }
569
570    /// Current §0.5 amplitude EMA weight; `0.0` means the smoother is
571    /// off.
572    #[inline]
573    pub fn amp_ema_alpha(&self) -> f64 {
574        self.analysis.amp_ema_alpha()
575    }
576
577    /// Configure the post-decoder enhancement chain. Off by default
578    /// ([`EnhancementMode::None`] — spec-faithful PCM). When set to
579    /// [`EnhancementMode::Classical`], decoded PCM passes through a
580    /// biquad cascade + soft-knee compressor + boundary-fade chain
581    /// before [`Self::decode_bits`] returns. See
582    /// [`crate::enhancement`] for stage details and AIC33 mapping.
583    ///
584    /// Resets the chain's runtime filter state (delay lines, envelope,
585    /// pending fade) so the new mode starts clean. Persistent: stays
586    /// configured across [`Self::reset`].
587    pub fn set_enhancement(&mut self, mode: EnhancementMode) {
588        self.enhancement = mode;
589        self.enhancement_state = EnhancementState::default();
590    }
591
592    /// Currently configured enhancement mode.
593    #[inline]
594    pub fn enhancement(&self) -> &EnhancementMode {
595        &self.enhancement
596    }
597
598    /// Start a fluent builder for this rate. Equivalent to
599    /// `VocoderBuilder::new(rate)`.
600    #[inline]
601    pub fn builder(rate: Rate) -> VocoderBuilder {
602        VocoderBuilder::new(rate)
603    }
604
605    /// The rate this channel was constructed at. Cannot change for
606    /// the lifetime of the channel; build a new [`Vocoder`] to switch
607    /// rates (mirrors a chip's PKT_RATEP cycle).
608    #[inline]
609    pub fn rate(&self) -> Rate {
610        self.rate
611    }
612
613    /// Number of i16 samples consumed per [`Self::encode_pcm`] call,
614    /// and produced per [`Self::decode_bits`] call.
615    #[inline]
616    pub fn frame_samples(&self) -> usize {
617        self.rate.frame_samples()
618    }
619
620    /// Number of FEC bytes per encoded frame at this rate.
621    #[inline]
622    pub fn fec_frame_bytes(&self) -> usize {
623        self.rate.fec_frame_bytes()
624    }
625
626    /// Read the most recent frame's stats. Returns the zero-default
627    /// before any frame has been processed.
628    #[inline]
629    pub fn last_stats(&self) -> &FrameStats {
630        &self.last_stats
631    }
632
633    /// Disposition (`Use` / `Repeat` / `Mute`) of the most recent
634    /// [`Self::decode_bits`] call's synthesizer pass. `None` until at
635    /// least one frame has been decoded. Reset by [`Self::reset`].
636    ///
637    /// Decode-side only; encode-side has no synth and always returns
638    /// `None`.
639    #[inline]
640    pub fn last_disposition(&self) -> Option<FrameDisposition> {
641        self.synth.last_disposition()
642    }
643
644    /// Reset all channel state. Equivalent to the chip's PKT_RATEP
645    /// re-send: clears predictor history, decoder predictor state,
646    /// synth substates, smoothed error rate, last-stats. Rate and
647    /// configuration knobs (tone detection) stay the same.
648    pub fn reset(&mut self) {
649        self.analysis = AnalysisState::new();
650        self.imbe_dec = imbe_wire::dequantize::DecoderState::new();
651        self.ambe_plus2_dec = ambe_plus2_wire::dequantize::DecoderState::new();
652        self.synth = SynthState::new();
653        self.last_stats = FrameStats::default();
654        self.enhancement_state = EnhancementState::default();
655        self.prev_disposition = None;
656        // tone_detection / enhancement: preserved (config, not state)
657    }
658
659    /// Encode one PCM frame into FEC-encoded bytes.
660    ///
661    /// `pcm` must be exactly [`Self::frame_samples`] samples (160).
662    /// Returns exactly [`Self::fec_frame_bytes`] bytes.
663    pub fn encode_pcm(&mut self, pcm: &[i16]) -> Result<Vec<u8>, VocoderError> {
664        if pcm.len() != self.frame_samples() {
665            return Err(VocoderError::WrongPcmLength {
666                expected: self.frame_samples(),
667                got: pcm.len(),
668            });
669        }
670        let (bytes, stats) = match self.rate {
671            Rate::Imbe7200x4400 => imbe_pipeline::encode(pcm, self, true)?,
672            Rate::Imbe4400x4400 => imbe_pipeline::encode(pcm, self, false)?,
673            Rate::AmbePlus2_3600x2450 => ambe_plus2_pipeline::encode(pcm, self, true)?,
674            Rate::AmbePlus2_2450x2450 => ambe_plus2_pipeline::encode(pcm, self, false)?,
675        };
676        self.last_stats.analysis = Some(stats);
677        Ok(bytes)
678    }
679
680    /// Decode one FEC-encoded frame into PCM samples.
681    ///
682    /// `bits` must be exactly [`Self::fec_frame_bytes`] bytes.
683    /// Returns exactly [`Self::frame_samples`] PCM samples.
684    pub fn decode_bits(&mut self, bits: &[u8]) -> Result<Vec<i16>, VocoderError> {
685        if bits.len() != self.fec_frame_bytes() {
686            return Err(VocoderError::WrongBitsLength {
687                expected: self.fec_frame_bytes(),
688                got: bits.len(),
689            });
690        }
691        let (mut pcm, stats) = match self.rate {
692            Rate::Imbe7200x4400 => imbe_pipeline::decode(bits, self, true),
693            Rate::Imbe4400x4400 => imbe_pipeline::decode(bits, self, false),
694            Rate::AmbePlus2_3600x2450 => ambe_plus2_pipeline::decode(bits, self, true),
695            Rate::AmbePlus2_2450x2450 => ambe_plus2_pipeline::decode(bits, self, false),
696        };
697        let prev_was_use = matches!(self.prev_disposition, Some(FrameDisposition::Use));
698        enhancement::apply(
699            &self.enhancement,
700            &mut self.enhancement_state,
701            &mut pcm,
702            SAMPLE_RATE_HZ,
703            prev_was_use,
704        );
705        self.prev_disposition = stats.disposition;
706        self.last_stats.decode = Some(stats);
707        Ok(pcm)
708    }
709
710    /// Encode an arbitrary-length PCM slice as a stream of frames.
711    ///
712    /// Returns an iterator that yields one `Result<Vec<u8>>` per
713    /// frame consumed (160 samples per frame). Trailing partial
714    /// frames are silently dropped — the caller is responsible for
715    /// padding to a multiple of [`Self::frame_samples`] if all
716    /// samples must be encoded.
717    ///
718    /// State (predictor, look-ahead history, ε_R) advances across
719    /// frames just as it would with manual per-frame
720    /// [`Self::encode_pcm`] calls.
721    ///
722    /// ```rust
723    /// # use blip25_mbe::vocoder::{Rate, Vocoder};
724    /// # let pcm: Vec<i16> = vec![0; 160 * 5];
725    /// let mut tx = Vocoder::new(Rate::Imbe7200x4400);
726    /// let bits: Result<Vec<Vec<u8>>, _> = tx.encode_stream(&pcm).collect();
727    /// assert_eq!(bits.unwrap().len(), 5);
728    /// ```
729    pub fn encode_stream<'a>(&'a mut self, pcm: &'a [i16]) -> EncodeStream<'a> {
730        EncodeStream { vocoder: self, pcm, pos: 0 }
731    }
732
733    /// Run the analysis encoder on one PCM frame and return the
734    /// resulting [`MbeParams`] without quantizing or FEC-encoding.
735    /// Advances the analysis state (look-ahead history, predictor,
736    /// V/UV state, silence detector) so subsequent calls see
737    /// continuous context.
738    ///
739    /// `pcm` must be exactly [`Self::frame_samples`] samples (160).
740    ///
741    /// On `AnalysisOutput::Silence` (preroll, silence dispatch,
742    /// half-rate `PitchOutOfRange`), returns the rate-appropriate
743    /// silence params ([`MbeParams::silence`] or
744    /// [`MbeParams::silence_ambe_plus2`]).
745    ///
746    /// Counterpart of [`Self::synthesize_params`]. Together these
747    /// expose the parameter layer without going through wire FEC,
748    /// enabling rate-conversion / analysis / playback pipelines that
749    /// don't need the full encode/decode round-trip.
750    pub fn extract_params(&mut self, pcm: &[i16]) -> Result<MbeParams, VocoderError> {
751        if pcm.len() != self.frame_samples() {
752            return Err(VocoderError::WrongPcmLength {
753                expected: self.frame_samples(),
754                got: pcm.len(),
755            });
756        }
757        let frame = pcm.try_into().expect("length already validated");
758        let analysis_out = match self.rate {
759            Rate::Imbe7200x4400 | Rate::Imbe4400x4400 => {
760                analysis_encode(frame, &mut self.analysis)
761            }
762            Rate::AmbePlus2_3600x2450 | Rate::AmbePlus2_2450x2450 => {
763                analysis_encode_ambe_plus2(frame, &mut self.analysis)
764            }
765        }
766        .map_err(VocoderError::Analysis)?;
767        Ok(match analysis_out {
768            AnalysisOutput::Voice(p) => p,
769            AnalysisOutput::Silence => match self.rate {
770                Rate::Imbe7200x4400 | Rate::Imbe4400x4400 => MbeParams::silence(),
771                Rate::AmbePlus2_3600x2450 | Rate::AmbePlus2_2450x2450 => {
772                    MbeParams::silence_ambe_plus2()
773                }
774            },
775        })
776    }
777
778    /// Synthesize one PCM frame directly from [`MbeParams`], skipping
779    /// the FEC + dequantize chain. Advances the channel's synth state
780    /// (so re-acquisition after silence and across-frame phase
781    /// continuity work the same way they would for a normal
782    /// [`Self::decode_bits`] call).
783    ///
784    /// Useful for playing back tone-frame params from
785    /// [`crate::ambe_plus2_wire::dequantize::tone_to_mbe_params`], replaying
786    /// captured params in test harnesses, or driving synth from any
787    /// upstream that produces `MbeParams` without going through wire
788    /// bits.
789    ///
790    /// The dispatch is rate-aware: full-rate uses BABA-A baseline
791    /// phase; half-rate uses AMBE+2 (US5701390) phase regen, matching
792    /// what [`Self::decode_bits`] would do.
793    ///
794    /// `last_stats` is **not** updated by this call — it's reserved
795    /// for the wire-aware encode/decode paths. Read the synth's
796    /// disposition via [`Self::last_disposition`] which IS advanced
797    /// (the disposition reflects this synth call).
798    pub fn synthesize_params(&mut self, params: &MbeParams) -> Vec<i16> {
799        // No FEC errors on this path — caller is providing trusted
800        // params, not recovered ones. Use a clean error context so
801        // disposition is `Use` unless smoothed ε_R is already high.
802        let prev_err = self.synth.err;
803        self.synth.err = FrameErrorContext::default();
804        let err = self.synth.err;
805        let gamma_w = self.synth.gamma_w;
806        let pcm: [i16; FRAME_SAMPLES] = match self.rate {
807            Rate::Imbe7200x4400 | Rate::Imbe4400x4400 => {
808                synthesize_frame(params, &err, gamma_w, &mut self.synth)
809            }
810            Rate::AmbePlus2_3600x2450 | Rate::AmbePlus2_2450x2450 => match self.ambe_plus2_synth {
811                AmbePlus2Synth::AmbePlus => ambe_plus2::synthesize_frame(params, &mut self.synth),
812                AmbePlus2Synth::Baseline => synthesize_frame(params, &err, gamma_w, &mut self.synth),
813            },
814        };
815        self.synth.err = prev_err;
816        let mut pcm = pcm.to_vec();
817        let prev_was_use = matches!(self.prev_disposition, Some(FrameDisposition::Use));
818        enhancement::apply(
819            &self.enhancement,
820            &mut self.enhancement_state,
821            &mut pcm,
822            SAMPLE_RATE_HZ,
823            prev_was_use,
824        );
825        self.prev_disposition = self.synth.last_disposition();
826        pcm
827    }
828
829    /// Decode an arbitrary-length FEC byte slice as a stream of PCM
830    /// frames.
831    ///
832    /// Returns an iterator that yields one `Result<Vec<i16>>` per
833    /// frame consumed ([`Self::fec_frame_bytes`] per frame). Trailing
834    /// partial frames are silently dropped.
835    ///
836    /// ```rust
837    /// # use blip25_mbe::vocoder::{Rate, Vocoder};
838    /// # let bits: Vec<u8> = vec![0; 18 * 5];
839    /// let mut rx = Vocoder::new(Rate::Imbe7200x4400);
840    /// let pcm_frames: Result<Vec<Vec<i16>>, _> = rx.decode_stream(&bits).collect();
841    /// assert_eq!(pcm_frames.unwrap().len(), 5);
842    /// ```
843    pub fn decode_stream<'a>(&'a mut self, bits: &'a [u8]) -> DecodeStream<'a> {
844        DecodeStream { vocoder: self, bits, pos: 0 }
845    }
846}
847
848/// Streaming-encode iterator returned by [`Vocoder::encode_stream`].
849/// Yields one `Result<Vec<u8>>` per 160-sample input frame; trailing
850/// partial frames are silently dropped.
851pub struct EncodeStream<'a> {
852    vocoder: &'a mut Vocoder,
853    pcm: &'a [i16],
854    pos: usize,
855}
856
857impl Iterator for EncodeStream<'_> {
858    type Item = Result<Vec<u8>, VocoderError>;
859    fn next(&mut self) -> Option<Self::Item> {
860        let n = self.vocoder.frame_samples();
861        if self.pos + n > self.pcm.len() {
862            return None;
863        }
864        let frame = &self.pcm[self.pos..self.pos + n];
865        self.pos += n;
866        Some(self.vocoder.encode_pcm(frame))
867    }
868
869    fn size_hint(&self) -> (usize, Option<usize>) {
870        let remaining = (self.pcm.len() - self.pos) / self.vocoder.frame_samples();
871        (remaining, Some(remaining))
872    }
873}
874
875impl ExactSizeIterator for EncodeStream<'_> {}
876
877/// Streaming-decode iterator returned by [`Vocoder::decode_stream`].
878/// Yields one `Result<Vec<i16>>` per FEC frame; trailing partial
879/// frames are silently dropped.
880pub struct DecodeStream<'a> {
881    vocoder: &'a mut Vocoder,
882    bits: &'a [u8],
883    pos: usize,
884}
885
886impl Iterator for DecodeStream<'_> {
887    type Item = Result<Vec<i16>, VocoderError>;
888    fn next(&mut self) -> Option<Self::Item> {
889        let n = self.vocoder.fec_frame_bytes();
890        if self.pos + n > self.bits.len() {
891            return None;
892        }
893        let frame = &self.bits[self.pos..self.pos + n];
894        self.pos += n;
895        Some(self.vocoder.decode_bits(frame))
896    }
897
898    fn size_hint(&self) -> (usize, Option<usize>) {
899        let remaining = (self.bits.len() - self.pos) / self.vocoder.fec_frame_bytes();
900        (remaining, Some(remaining))
901    }
902}
903
904impl ExactSizeIterator for DecodeStream<'_> {}
905
906/// Direction of a [`Transcoder`] — the input/output rate pair.
907///
908/// Scales O(N) for N rates instead of O(N²) enum variants. Not every
909/// `(from, to)` pair has a wired parameter-domain converter; see
910/// [`Transcoder::new`] for the supported set.
911#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
912#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
913pub struct TranscodeDirection {
914    /// Rate of the input FEC frame.
915    pub from: Rate,
916    /// Rate of the output FEC frame.
917    pub to: Rate,
918}
919
920impl TranscodeDirection {
921    /// Construct a direction from a `(from, to)` pair.
922    #[inline]
923    pub const fn new(from: Rate, to: Rate) -> Self {
924        Self { from, to }
925    }
926
927    /// Bytes per input FEC frame for this direction.
928    #[inline]
929    pub const fn input_frame_bytes(self) -> usize {
930        self.from.fec_frame_bytes()
931    }
932
933    /// Bytes per output FEC frame for this direction.
934    #[inline]
935    pub const fn output_frame_bytes(self) -> usize {
936        self.to.fec_frame_bytes()
937    }
938}
939
940/// Bridge P25 Phase 1 ↔ P25 Phase 2 at the wire-bits layer.
941///
942/// Internally the transcoder runs the parameter-domain converter
943/// (BABA-A §11 — extract params from one rate's bits, then re-quantize
944/// at the other rate without any PCM round-trip). Avoids the
945/// 8-kHz-PCM detour, so quality stays at the parameter-extraction
946/// floor rather than going through analysis-encode → synthesis →
947/// analysis-encode again.
948///
949/// Direction is fixed at construction. State (cross-rate predictor,
950/// last-good frame for repeats) is internal and advances per call.
951///
952/// ```rust
953/// use blip25_mbe::vocoder::{Rate, Transcoder};
954///
955/// let mut tx = Transcoder::new(Rate::Imbe7200x4400, Rate::AmbePlus2_3600x2450).unwrap();
956/// let phase1_bits: [u8; 18] = [0; 18];
957/// let phase2_bits = tx.transcode(&phase1_bits).unwrap();
958/// assert_eq!(phase2_bits.len(), 9);
959/// ```
960pub struct Transcoder {
961    direction: TranscodeDirection,
962    full_to_half: Option<crate::rate_conversion::FullToHalfConverter>,
963    half_to_full: Option<crate::rate_conversion::HalfToFullConverter>,
964}
965
966impl Transcoder {
967    /// Open a new transcoder for the `(from, to)` rate pair.
968    ///
969    /// Cross-codec pairs (run the parameter-domain converter):
970    /// - `(Rate::Imbe7200x4400, Rate::AmbePlus2_3600x2450)`
971    /// - `(Rate::AmbePlus2_3600x2450, Rate::Imbe7200x4400)`
972    ///
973    /// Same-codec FEC ↔ no-FEC pairs (pure wire-layer bit shuffling,
974    /// no codec or predictor state):
975    /// - `(Rate::Imbe7200x4400, Rate::Imbe4400x4400)` — strip Annex H FEC
976    /// - `(Rate::Imbe4400x4400, Rate::Imbe7200x4400)` — add Annex H FEC
977    /// - `(Rate::AmbePlus2_3600x2450, Rate::AmbePlus2_2450x2450)` — strip half-rate FEC
978    /// - `(Rate::AmbePlus2_2450x2450, Rate::AmbePlus2_3600x2450)` — add half-rate FEC
979    ///
980    /// Any other combination returns
981    /// [`VocoderError::UnsupportedTranscode`].
982    pub fn new(from: Rate, to: Rate) -> Result<Self, VocoderError> {
983        let direction = TranscodeDirection { from, to };
984        match (from, to) {
985            (Rate::Imbe7200x4400, Rate::AmbePlus2_3600x2450) => Ok(Self {
986                direction,
987                full_to_half: Some(crate::rate_conversion::FullToHalfConverter::new()),
988                half_to_full: None,
989            }),
990            (Rate::AmbePlus2_3600x2450, Rate::Imbe7200x4400) => Ok(Self {
991                direction,
992                full_to_half: None,
993                half_to_full: Some(crate::rate_conversion::HalfToFullConverter::new()),
994            }),
995            // Same-codec FEC ↔ no-FEC pairs are stateless wire transforms;
996            // both converters stay None.
997            (Rate::Imbe7200x4400, Rate::Imbe4400x4400)
998            | (Rate::Imbe4400x4400, Rate::Imbe7200x4400)
999            | (Rate::AmbePlus2_3600x2450, Rate::AmbePlus2_2450x2450)
1000            | (Rate::AmbePlus2_2450x2450, Rate::AmbePlus2_3600x2450) => Ok(Self {
1001                direction,
1002                full_to_half: None,
1003                half_to_full: None,
1004            }),
1005            _ => Err(VocoderError::UnsupportedTranscode { from, to }),
1006        }
1007    }
1008
1009    /// Direction this transcoder was opened in.
1010    #[inline]
1011    pub fn direction(&self) -> TranscodeDirection {
1012        self.direction
1013    }
1014
1015    /// Transcode one input FEC frame to one output FEC frame.
1016    ///
1017    /// `bits` must be exactly [`TranscodeDirection::input_frame_bytes`]
1018    /// long; the returned `Vec` has exactly
1019    /// [`TranscodeDirection::output_frame_bytes`].
1020    pub fn transcode(&mut self, bits: &[u8]) -> Result<Vec<u8>, VocoderError> {
1021        let in_n = self.direction.input_frame_bytes();
1022        if bits.len() != in_n {
1023            return Err(VocoderError::WrongBitsLength {
1024                expected: in_n,
1025                got: bits.len(),
1026            });
1027        }
1028        match (self.direction.from, self.direction.to) {
1029            (Rate::Imbe7200x4400, Rate::AmbePlus2_3600x2450) => {
1030                let dibits_in = unpack_dibits_n::<72>(bits);
1031                let dibits_out = self
1032                    .full_to_half
1033                    .as_mut()
1034                    .expect("constructed with this direction")
1035                    .convert(&dibits_in)
1036                    .map_err(|e| VocoderError::Quantize(format!("{e:?}")))?;
1037                Ok(pack_dibits_n::<36, 9>(&dibits_out).to_vec())
1038            }
1039            (Rate::AmbePlus2_3600x2450, Rate::Imbe7200x4400) => {
1040                let dibits_in = unpack_dibits_n::<36>(bits);
1041                let dibits_out = self
1042                    .half_to_full
1043                    .as_mut()
1044                    .expect("constructed with this direction")
1045                    .convert(&dibits_in)
1046                    .map_err(|e| VocoderError::Quantize(format!("{e:?}")))?;
1047                Ok(pack_dibits_n::<72, 18>(&dibits_out).to_vec())
1048            }
1049            (Rate::Imbe7200x4400, Rate::Imbe4400x4400) => {
1050                Ok(imbe_pipeline::fec_to_info_bytes(bits).to_vec())
1051            }
1052            (Rate::Imbe4400x4400, Rate::Imbe7200x4400) => {
1053                Ok(imbe_pipeline::info_to_fec_bytes(bits).to_vec())
1054            }
1055            (Rate::AmbePlus2_3600x2450, Rate::AmbePlus2_2450x2450) => {
1056                Ok(ambe_plus2_pipeline::fec_to_info_bytes(bits).to_vec())
1057            }
1058            (Rate::AmbePlus2_2450x2450, Rate::AmbePlus2_3600x2450) => {
1059                Ok(ambe_plus2_pipeline::info_to_fec_bytes(bits).to_vec())
1060            }
1061            (from, to) => Err(VocoderError::UnsupportedTranscode { from, to }),
1062        }
1063    }
1064
1065    /// Reset all transcoder state. Equivalent to opening a fresh
1066    /// channel.
1067    pub fn reset(&mut self) {
1068        *self = Self::new(self.direction.from, self.direction.to)
1069            .expect("direction was validated at construction");
1070    }
1071}
1072
1073fn unpack_dibits_n<const N: usize>(bytes: &[u8]) -> [u8; N] {
1074    let mut out = [0u8; N];
1075    let mut bit = 0usize;
1076    for slot in &mut out {
1077        let mut d = 0u8;
1078        for _ in 0..2 {
1079            let b = (bytes[bit / 8] >> (7 - (bit % 8))) & 1;
1080            d = (d << 1) | b;
1081            bit += 1;
1082        }
1083        *slot = d;
1084    }
1085    out
1086}
1087
1088fn pack_dibits_n<const N: usize, const B: usize>(dibits: &[u8; N]) -> [u8; B] {
1089    let mut out = [0u8; B];
1090    let mut bit = 0usize;
1091    for &d in dibits {
1092        for pos in (0..2).rev() {
1093            let b = (d >> pos) & 1;
1094            out[bit / 8] |= b << (7 - (bit % 8));
1095            bit += 1;
1096        }
1097    }
1098    out
1099}
1100
1101/// Push-driven encoder for live PCM streams that arrive in chunks
1102/// of arbitrary length (audio device callbacks, file readers, sockets).
1103/// Holds residual samples internally across calls so the caller can
1104/// push whatever they have and harvest frames as they become available.
1105///
1106/// One-frame-at-a-time `Vocoder` is the right primitive for callers
1107/// that already have whole-buffer PCM. `LiveEncoder` is for callers
1108/// that don't.
1109///
1110/// ```rust
1111/// # use blip25_mbe::vocoder::{LiveEncoder, Rate};
1112/// let mut enc = LiveEncoder::new(Rate::Imbe7200x4400);
1113/// // 256 samples (audio-device callback); not a multiple of 160.
1114/// let chunk: [i16; 256] = [0; 256];
1115/// let frames = enc.push(&chunk);
1116/// assert_eq!(frames.len(), 1);                   // one full frame produced
1117/// assert!(frames[0].is_ok());
1118/// // 96 samples residue; next push contributes them to the next frame.
1119/// assert_eq!(enc.pending_samples(), 96);
1120/// ```
1121pub struct LiveEncoder {
1122    vocoder: Vocoder,
1123    pcm_buf: Vec<i16>,
1124}
1125
1126impl LiveEncoder {
1127    /// Open a new live encoder at the given rate, all state cold.
1128    pub fn new(rate: Rate) -> Self {
1129        Self {
1130            vocoder: Vocoder::new(rate),
1131            pcm_buf: Vec::new(),
1132        }
1133    }
1134
1135    /// Read-only access to the underlying [`Vocoder`] (for stats /
1136    /// rate / disposition queries).
1137    #[inline]
1138    pub fn vocoder(&self) -> &Vocoder {
1139        &self.vocoder
1140    }
1141
1142    /// Append PCM samples and emit zero or more FEC frames. Per-frame
1143    /// errors are surfaced as `Err` entries in the returned Vec; the
1144    /// buffer drains regardless so a single bad frame doesn't stall
1145    /// the stream.
1146    pub fn push(&mut self, pcm: &[i16]) -> Vec<Result<Vec<u8>, VocoderError>> {
1147        self.pcm_buf.extend_from_slice(pcm);
1148        let n = self.vocoder.frame_samples();
1149        let mut out = Vec::with_capacity(self.pcm_buf.len() / n);
1150        while self.pcm_buf.len() >= n {
1151            // Encode from the front of the buffer; drain after the call.
1152            // Disjoint-field borrow keeps this clean (vocoder and
1153            // pcm_buf are independent struct fields).
1154            let result = self.vocoder.encode_pcm(&self.pcm_buf[..n]);
1155            self.pcm_buf.drain(..n);
1156            out.push(result);
1157        }
1158        out
1159    }
1160
1161    /// Number of samples currently buffered (between 0 and
1162    /// `frame_samples()-1` after every `push` returns).
1163    #[inline]
1164    pub fn pending_samples(&self) -> usize {
1165        self.pcm_buf.len()
1166    }
1167
1168    /// Drop any pending samples without encoding them. Useful at
1169    /// stream shutdown when the caller doesn't want a partial-frame
1170    /// flush.
1171    #[inline]
1172    pub fn discard_pending(&mut self) {
1173        self.pcm_buf.clear();
1174    }
1175
1176    /// Pad pending residue with zeros to a full frame and encode one
1177    /// final frame. Returns `Ok(Some(bits))` if residue existed,
1178    /// `Ok(None)` if the buffer was empty. Buffer is drained either
1179    /// way.
1180    ///
1181    /// Use this at end-of-stream to avoid abruptly dropping the
1182    /// trailing samples; the cost is at most one extra frame of
1183    /// zero-padding tacked onto the last word of audio.
1184    pub fn flush(&mut self) -> Result<Option<Vec<u8>>, VocoderError> {
1185        if self.pcm_buf.is_empty() {
1186            return Ok(None);
1187        }
1188        let n = self.vocoder.frame_samples();
1189        self.pcm_buf.resize(n, 0);
1190        let bits = self.vocoder.encode_pcm(&self.pcm_buf)?;
1191        self.pcm_buf.clear();
1192        Ok(Some(bits))
1193    }
1194
1195    /// Reset all state — both the inner [`Vocoder`] (predictor /
1196    /// look-ahead / synth substates) and the residual sample buffer.
1197    pub fn reset(&mut self) {
1198        self.vocoder.reset();
1199        self.pcm_buf.clear();
1200    }
1201
1202    /// Configured rate.
1203    #[inline]
1204    pub fn rate(&self) -> Rate {
1205        self.vocoder.rate()
1206    }
1207}
1208
1209/// Push-driven decoder for live FEC byte streams that arrive in chunks
1210/// of arbitrary length (network sockets, log replays, partial reads).
1211/// Mirrors [`LiveEncoder`].
1212///
1213/// ```rust
1214/// # use blip25_mbe::vocoder::{LiveDecoder, Rate};
1215/// let mut dec = LiveDecoder::new(Rate::AmbePlus2_3600x2450);
1216/// let chunk: [u8; 23] = [0; 23];   // 2 full 9-byte frames + 5 byte residue
1217/// let frames = dec.push(&chunk);
1218/// assert_eq!(frames.len(), 2);
1219/// assert_eq!(dec.pending_bytes(), 5);
1220/// ```
1221pub struct LiveDecoder {
1222    vocoder: Vocoder,
1223    bits_buf: Vec<u8>,
1224}
1225
1226impl LiveDecoder {
1227    /// Open a new live decoder at the given rate, all state cold.
1228    pub fn new(rate: Rate) -> Self {
1229        Self {
1230            vocoder: Vocoder::new(rate),
1231            bits_buf: Vec::new(),
1232        }
1233    }
1234
1235    /// Read-only access to the underlying [`Vocoder`].
1236    #[inline]
1237    pub fn vocoder(&self) -> &Vocoder {
1238        &self.vocoder
1239    }
1240
1241    /// Append FEC bytes and emit zero or more PCM frames. Per-frame
1242    /// errors surface as `Err` entries in the returned Vec; the
1243    /// buffer drains regardless.
1244    pub fn push(&mut self, bits: &[u8]) -> Vec<Result<Vec<i16>, VocoderError>> {
1245        self.bits_buf.extend_from_slice(bits);
1246        let n = self.vocoder.fec_frame_bytes();
1247        let mut out = Vec::with_capacity(self.bits_buf.len() / n);
1248        while self.bits_buf.len() >= n {
1249            let result = self.vocoder.decode_bits(&self.bits_buf[..n]);
1250            self.bits_buf.drain(..n);
1251            out.push(result);
1252        }
1253        out
1254    }
1255
1256    /// Bytes currently buffered (between 0 and `fec_frame_bytes()-1`
1257    /// after every `push` returns).
1258    #[inline]
1259    pub fn pending_bytes(&self) -> usize {
1260        self.bits_buf.len()
1261    }
1262
1263    /// Drop any pending bytes without decoding them.
1264    #[inline]
1265    pub fn discard_pending(&mut self) {
1266        self.bits_buf.clear();
1267    }
1268
1269    /// Reset all state — inner [`Vocoder`] + residual byte buffer.
1270    pub fn reset(&mut self) {
1271        self.vocoder.reset();
1272        self.bits_buf.clear();
1273    }
1274
1275    /// Configured rate.
1276    #[inline]
1277    pub fn rate(&self) -> Rate {
1278        self.vocoder.rate()
1279    }
1280}
1281
1282/// Fluent builder for [`Vocoder`]. Lets callers configure rate +
1283/// optional knobs in one expression instead of `new` + a sequence of
1284/// setters. Mirrors the chip's "open channel + PKT_RATEP +
1285/// configuration" sequence, but in one call.
1286///
1287/// ```rust
1288/// use blip25_mbe::vocoder::{Rate, Vocoder};
1289///
1290/// let tx = Vocoder::builder(Rate::AmbePlus2_3600x2450)
1291///     .tone_detection(true)
1292///     .build();
1293/// assert_eq!(tx.rate(), Rate::AmbePlus2_3600x2450);
1294/// assert!(tx.tone_detection());
1295/// ```
1296#[derive(Clone, Debug)]
1297pub struct VocoderBuilder {
1298    rate: Rate,
1299    tone_detection: bool,
1300    repeat_reset_after: Option<u32>,
1301    chip_compat: bool,
1302    silence_dispatch: bool,
1303    pitch_silence_override: bool,
1304    default_pitch_on_silence: bool,
1305    pyin_pitch: bool,
1306    spectral_subtraction: bool,
1307    amp_ema_alpha: f64,
1308    ambe_plus2_synth: AmbePlus2Synth,
1309    enhancement: EnhancementMode,
1310}
1311
1312impl VocoderBuilder {
1313    /// New builder defaulting to the same configuration as
1314    /// [`Vocoder::new`] — i.e. Classical enhancement chain and §0.5
1315    /// spectral subtraction are ON, since both are free PESQ wins
1316    /// (see 5-vector A/B 2026-05-14). All other knobs default OFF /
1317    /// spec-faithful. Use the setters to opt out (e.g.
1318    /// `.enhancement(EnhancementMode::None).spectral_subtraction(false)`
1319    /// for fully spec-faithful output).
1320    #[inline]
1321    pub fn new(rate: Rate) -> Self {
1322        Self {
1323            rate,
1324            tone_detection: false,
1325            repeat_reset_after: None,
1326            chip_compat: false,
1327            silence_dispatch: false,
1328            pitch_silence_override: false,
1329            default_pitch_on_silence: false,
1330            pyin_pitch: false,
1331            spectral_subtraction: true,
1332            amp_ema_alpha: 0.0,
1333            ambe_plus2_synth: AmbePlus2Synth::AmbePlus,
1334            enhancement: EnhancementMode::Classical(
1335                crate::enhancement::ClassicalConfig::default(),
1336            ),
1337        }
1338    }
1339
1340    /// Enable encode-side Annex T tone detection (half-rate only).
1341    /// See [`Vocoder::set_tone_detection`].
1342    #[inline]
1343    pub fn tone_detection(mut self, on: bool) -> Self {
1344        self.tone_detection = on;
1345        self
1346    }
1347
1348    /// Configure the beyond-spec consecutive-repeat reset threshold.
1349    /// See [`Vocoder::set_repeat_reset_after`].
1350    #[inline]
1351    pub fn repeat_reset_after(mut self, n: Option<u32>) -> Self {
1352        self.repeat_reset_after = n;
1353        self
1354    }
1355
1356    /// Enable JMBE-style error-rate freeze on Repeat (gap 0021).
1357    /// See [`Vocoder::set_chip_compat`].
1358    #[inline]
1359    pub fn chip_compat(mut self, on: bool) -> Self {
1360        self.chip_compat = on;
1361        self
1362    }
1363
1364    /// Enable §0.8.4 silence dispatch on the analysis encoder.
1365    /// See [`Vocoder::set_silence_dispatch`].
1366    #[inline]
1367    pub fn silence_dispatch(mut self, on: bool) -> Self {
1368        self.silence_dispatch = on;
1369        self
1370    }
1371
1372    /// Enable the joint-signal silence override (requires
1373    /// [`Self::silence_dispatch`] also on).
1374    /// See [`Vocoder::set_pitch_silence_override`].
1375    #[inline]
1376    pub fn pitch_silence_override(mut self, on: bool) -> Self {
1377        self.pitch_silence_override = on;
1378        self
1379    }
1380
1381    /// Enable the onset-attack mitigation (commit a short default
1382    /// pitch on near-silent frames). See
1383    /// [`Vocoder::set_default_pitch_on_silence`].
1384    #[inline]
1385    pub fn default_pitch_on_silence(mut self, on: bool) -> Self {
1386        self.default_pitch_on_silence = on;
1387        self
1388    }
1389
1390    /// Enable the PYIN pitch frontend (post-2002 DSP, off by default).
1391    /// See [`Vocoder::set_pyin_pitch`].
1392    #[inline]
1393    pub fn pyin_pitch(mut self, on: bool) -> Self {
1394        self.pyin_pitch = on;
1395        self
1396    }
1397
1398    /// Enable spectral subtraction at the §0.5 amplitude input.
1399    /// See [`Vocoder::set_spectral_subtraction`].
1400    #[inline]
1401    pub fn spectral_subtraction(mut self, on: bool) -> Self {
1402        self.spectral_subtraction = on;
1403        self
1404    }
1405
1406    /// Configure the §0.5 amplitude EMA weight (0.0 = off; default).
1407    /// See [`Vocoder::set_amp_ema_alpha`].
1408    #[inline]
1409    pub fn amp_ema_alpha(mut self, alpha: f64) -> Self {
1410        self.amp_ema_alpha = alpha;
1411        self
1412    }
1413
1414    /// Configure the half-rate synth flavor (no-op for full-rate).
1415    /// See [`Vocoder::set_ambe_plus2_synth`].
1416    #[inline]
1417    pub fn ambe_plus2_synth(mut self, gen: AmbePlus2Synth) -> Self {
1418        self.ambe_plus2_synth = gen;
1419        self
1420    }
1421
1422    /// Configure the post-decoder enhancement chain.
1423    /// See [`Vocoder::set_enhancement`].
1424    #[inline]
1425    pub fn enhancement(mut self, mode: EnhancementMode) -> Self {
1426        self.enhancement = mode;
1427        self
1428    }
1429
1430    /// Materialize the [`Vocoder`].
1431    pub fn build(self) -> Vocoder {
1432        let mut v = Vocoder::new(self.rate);
1433        v.set_tone_detection(self.tone_detection);
1434        v.set_repeat_reset_after(self.repeat_reset_after);
1435        v.set_chip_compat(self.chip_compat);
1436        v.set_silence_dispatch(self.silence_dispatch);
1437        v.set_pitch_silence_override(self.pitch_silence_override);
1438        v.set_default_pitch_on_silence(self.default_pitch_on_silence);
1439        v.set_pyin_pitch(self.pyin_pitch);
1440        v.set_spectral_subtraction(self.spectral_subtraction);
1441        v.set_amp_ema_alpha(self.amp_ema_alpha);
1442        v.set_ambe_plus2_synth(self.ambe_plus2_synth);
1443        v.set_enhancement(self.enhancement);
1444        v
1445    }
1446}
1447
1448impl core::fmt::Debug for Vocoder {
1449    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1450        f.debug_struct("Vocoder")
1451            .field("rate", &self.rate)
1452            .field("frame_samples", &self.frame_samples())
1453            .field("fec_frame_bytes", &self.fec_frame_bytes())
1454            .finish_non_exhaustive()
1455    }
1456}
1457
1458mod imbe_pipeline {
1459    use super::*;
1460    use crate::imbe_wire::dequantize::{dequantize, quantize};
1461    use crate::imbe_wire::frame::{INFO_WIDTHS, decode_frame, encode_frame};
1462    use crate::imbe_wire::priority::{deprioritize, prioritize};
1463
1464    pub(super) fn encode(
1465        pcm: &[i16],
1466        vocoder: &mut Vocoder,
1467        apply_fec: bool,
1468    ) -> Result<(Vec<u8>, AnalysisStats), VocoderError> {
1469        // Phase 1 has no tone-frame opcode at the wire layer, but with
1470        // detection enabled we still run the detector and surface the
1471        // (I_D, A_D) result via AnalysisStats::tone_detect so consumers
1472        // can route it to LCW / app-layer signaling per BABA-A §5.4.
1473        let tone_detect = if vocoder.tone_detection {
1474            detect_tone(pcm)
1475        } else {
1476            None
1477        };
1478        let frame = pcm.try_into().expect("length already validated");
1479        let (kind, params) = match analysis_encode(frame, &mut vocoder.analysis)
1480            .map_err(VocoderError::Analysis)?
1481        {
1482            AnalysisOutput::Voice(p) => (AnalysisOutputKind::Voice, p),
1483            AnalysisOutput::Silence => (AnalysisOutputKind::Silence, MbeParams::silence()),
1484        };
1485        let mut snapshot = vocoder.imbe_dec.clone();
1486        let b = quantize(&params, &mut vocoder.imbe_dec)
1487            .map_err(|e| VocoderError::Quantize(format!("{e:?}")))?;
1488        // Step the decoder snapshot via dequantize so the predictor
1489        // state matches what a downstream receiver would observe (the
1490        // existing speech-quality harness pattern).
1491        let l = params.harmonic_count();
1492        let info = prioritize(&b, l);
1493        let _ = deprioritize; // reserved for future per-priority diagnostics
1494        let _ = dequantize(&info, &mut snapshot);
1495        vocoder.imbe_dec = snapshot;
1496        let bytes: Vec<u8> = if apply_fec {
1497            let dibits = encode_frame(&info);
1498            pack_dibits_full(&dibits).to_vec()
1499        } else {
1500            pack_info_full(&info).to_vec()
1501        };
1502        Ok((bytes, AnalysisStats { output: kind, params, tone_detect }))
1503    }
1504
1505    pub(super) fn decode(
1506        bits: &[u8],
1507        vocoder: &mut Vocoder,
1508        apply_fec: bool,
1509    ) -> (Vec<i16>, DecodeStats) {
1510        let (info, stats_eps0, stats_epst, eps4) = if apply_fec {
1511            let dibits = analysis_profile::time(analysis_profile::Stage::DibitUnpack, || {
1512                unpack_dibits_full(bits)
1513            });
1514            let imbe = analysis_profile::time(analysis_profile::Stage::DecodeFrame, || {
1515                decode_frame(&dibits)
1516            });
1517            let s0: u8 = imbe.errors[0];
1518            let st: u8 = imbe.error_total().min(255) as u8;
1519            let e4: u8 = imbe.errors[4];
1520            (imbe.info, s0, st, e4)
1521        } else {
1522            // No FEC layer — info bits arrive verbatim, error counts are zero.
1523            let info = unpack_info_full(bits);
1524            (info, 0u8, 0u8, 0u8)
1525        };
1526        let dq = analysis_profile::time(analysis_profile::Stage::Dequantize, || {
1527            dequantize(&info, &mut vocoder.imbe_dec)
1528        });
1529        let pcm: [i16; FRAME_SAMPLES] = match dq {
1530            Ok(params) => {
1531                let err = FrameErrorContext {
1532                    epsilon_0: stats_eps0,
1533                    epsilon_4: eps4,
1534                    epsilon_t: stats_epst,
1535                    bad_pitch: false,
1536                };
1537                synthesize_frame(&params, &err, GAMMA_W, &mut vocoder.synth)
1538            }
1539            Err(_) => [0i16; FRAME_SAMPLES],
1540        };
1541        let disposition = vocoder.synth.last_disposition();
1542        (
1543            pcm.to_vec(),
1544            DecodeStats {
1545                epsilon_0: stats_eps0,
1546                epsilon_t: stats_epst,
1547                disposition,
1548            },
1549        )
1550    }
1551
1552    fn pack_info_full(info: &[u16; 8]) -> [u8; 11] {
1553        let mut out = [0u8; 11];
1554        super::pack_info_bits(info, &INFO_WIDTHS, &mut out);
1555        out
1556    }
1557
1558    fn unpack_info_full(bytes: &[u8]) -> [u16; 8] {
1559        let mut out = [0u16; 8];
1560        super::unpack_info_bits(bytes, &INFO_WIDTHS, &mut out);
1561        out
1562    }
1563
1564    /// Wire-layer transcode: 18-byte FEC frame → 11-byte info-only
1565    /// frame. Pure bit-shuffling — no codec or predictor state.
1566    pub(super) fn fec_to_info_bytes(fec_bytes: &[u8]) -> [u8; 11] {
1567        let dibits = unpack_dibits_full(fec_bytes);
1568        let frame = decode_frame(&dibits);
1569        pack_info_full(&frame.info)
1570    }
1571
1572    /// Wire-layer transcode: 11-byte info-only frame → 18-byte FEC
1573    /// frame. Pure bit-shuffling — re-applies Golay/Hamming/PN.
1574    pub(super) fn info_to_fec_bytes(info_bytes: &[u8]) -> [u8; 18] {
1575        let info = unpack_info_full(info_bytes);
1576        let dibits = encode_frame(&info);
1577        pack_dibits_full(&dibits)
1578    }
1579
1580    fn pack_dibits_full(dibits: &[u8; 72]) -> [u8; 18] {
1581        let mut out = [0u8; 18];
1582        let mut bit = 0usize;
1583        for &d in dibits {
1584            for pos in (0..2).rev() {
1585                let b = (d >> pos) & 1;
1586                out[bit / 8] |= b << (7 - (bit % 8));
1587                bit += 1;
1588            }
1589        }
1590        out
1591    }
1592
1593    fn unpack_dibits_full(bytes: &[u8]) -> [u8; 72] {
1594        let mut out = [0u8; 72];
1595        let mut bit = 0usize;
1596        for slot in &mut out {
1597            let mut d = 0u8;
1598            for _ in 0..2 {
1599                let b = (bytes[bit / 8] >> (7 - (bit % 8))) & 1;
1600                d = (d << 1) | b;
1601                bit += 1;
1602            }
1603            *slot = d;
1604        }
1605        out
1606    }
1607}
1608
1609mod ambe_plus2_pipeline {
1610    use super::*;
1611    use crate::ambe_plus2_wire::dequantize::{
1612        Decoded, decode_to_params, encode_tone_frame_info, quantize, tone_to_mbe_params,
1613    };
1614    use crate::ambe_plus2_wire::frame::{INFO_WIDTHS, decode_frame, encode_frame};
1615
1616    pub(super) fn encode(
1617        pcm: &[i16],
1618        vocoder: &mut Vocoder,
1619        apply_fec: bool,
1620    ) -> Result<(Vec<u8>, AnalysisStats), VocoderError> {
1621        // Tone-detect dispatch (opt-in). On a hit, bypass the voice
1622        // analysis pipeline entirely and emit an Annex T tone frame.
1623        // detect_tone tries DTMF (l1 != l2) first then falls through
1624        // to single-tone (l1 == l2 == 1).
1625        let tone_detect = if vocoder.tone_detection {
1626            detect_tone(pcm)
1627        } else {
1628            None
1629        };
1630        if let Some(ToneDetection { id, amplitude }) = tone_detect {
1631            let info = encode_tone_frame_info(id, amplitude);
1632            let bytes = pack_info_or_fec(&info, apply_fec);
1633            // Reconstruct the params the decoder will see, so
1634            // FrameStats carries the actual audible content.
1635            // tone_to_mbe_params returns Some for any valid Annex T
1636            // row; for the unlikely None case (reserved id), fall
1637            // back to a half-rate-friendly silence placeholder.
1638            let params = tone_to_mbe_params(id, amplitude)
1639                .unwrap_or_else(MbeParams::silence_ambe_plus2);
1640            return Ok((
1641                bytes,
1642                AnalysisStats {
1643                    output: AnalysisOutputKind::Tone { id, amplitude },
1644                    params,
1645                    tone_detect,
1646                },
1647            ));
1648        }
1649
1650        let frame = pcm.try_into().expect("length already validated");
1651        let (kind, params) = match analysis_encode_ambe_plus2(frame, &mut vocoder.analysis)
1652            .map_err(VocoderError::Analysis)?
1653        {
1654            AnalysisOutput::Voice(p) => (AnalysisOutputKind::Voice, p),
1655            AnalysisOutput::Silence => (AnalysisOutputKind::Silence, MbeParams::silence_ambe_plus2()),
1656        };
1657        let info = quantize(&params, &mut vocoder.ambe_plus2_dec)
1658            .map_err(|e| VocoderError::Quantize(format!("{e:?}")))?;
1659        let bytes = pack_info_or_fec(&info, apply_fec);
1660        Ok((bytes, AnalysisStats { output: kind, params, tone_detect }))
1661    }
1662
1663    fn pack_info_or_fec(info: &[u16; 4], apply_fec: bool) -> Vec<u8> {
1664        if apply_fec {
1665            let dibits = encode_frame(info);
1666            pack_dibits_half(&dibits).to_vec()
1667        } else {
1668            pack_info_half(info).to_vec()
1669        }
1670    }
1671
1672    pub(super) fn decode(
1673        bits: &[u8],
1674        vocoder: &mut Vocoder,
1675        apply_fec: bool,
1676    ) -> (Vec<i16>, DecodeStats) {
1677        let (info, stats_eps0, stats_epst, eps3) = if apply_fec {
1678            let dibits = analysis_profile::time(analysis_profile::Stage::DibitUnpack, || {
1679                unpack_dibits_half(bits)
1680            });
1681            let frame = analysis_profile::time(analysis_profile::Stage::DecodeFrame, || {
1682                decode_frame(&dibits)
1683            });
1684            // BABA-A §2.8.1 Eq. 196: half-rate ε_T = ε₀ + ε₁ only (the
1685            // two Golay codewords). The Hamming-protected ε₂/ε₃ are *not*
1686            // summed into the disposition's ε_T per the spec, even though
1687            // they're reported separately for diagnostics. Summing all
1688            // four cosets here was a port of the full-rate convention
1689            // and miscalibrates the Repeat/Mute thresholds for half-rate.
1690            //
1691            // Uncorrectable Golay-24 (≥4 errors detected) is reported as
1692            // ε₀ = u8::MAX from our FEC decoder. Per chip A/B (5/2026):
1693            // controlled 4-error c̃₀ injects keep the chip at peak ~5800
1694            // without a multi-frame Mute, so the chip caps the contribution
1695            // to ε_R. We model that as "ε₀ → 4": still trips Repeat via
1696            // the ε₀ ≥ 4 branch of Eq. 198, but the ε_R recurrence Eq. 197
1697            // (0.95·prev + 0.001064·ε_T) stays well below 0.096 instead of
1698            // climbing to 0.27 and Muting ~20 frames.
1699            const E0_UNCORRECTABLE_CAP: u8 = 4;
1700            let raw_e0 = frame.errors[0];
1701            let s0: u8 = if raw_e0 == u8::MAX { E0_UNCORRECTABLE_CAP } else { raw_e0 };
1702            let s1: u8 = frame.errors[1];
1703            let st: u8 = u16::from(s0)
1704                .saturating_add(u16::from(s1))
1705                .min(255) as u8;
1706            let e3: u8 = frame.errors[3];
1707            (frame.info, s0, st, e3)
1708        } else {
1709            // No FEC layer — info bits arrive verbatim, error counts are zero.
1710            let info = unpack_info_half(bits);
1711            (info, 0u8, 0u8, 0u8)
1712        };
1713        let err = FrameErrorContext {
1714            epsilon_0: stats_eps0,
1715            epsilon_4: eps3, // half-rate has 4 codewords; index 3 = û₃
1716            epsilon_t: stats_epst,
1717            bad_pitch: false,
1718        };
1719        // Publish the per-frame err context to the synth state so the
1720        // AmbePlus path (which reads `state.err` rather than taking an
1721        // explicit err parameter) sees the actual FEC counts. Without
1722        // this, AmbePlus synth always saw the default-zero err and the
1723        // §2.8 Repeat/Mute thresholds never tripped, allowing spurious
1724        // post-FEC transients (call_3537 frame 773 → peak 23553).
1725        vocoder.synth.err = err;
1726        let dq = analysis_profile::time(analysis_profile::Stage::Dequantize, || {
1727            decode_to_params(&info, &mut vocoder.ambe_plus2_dec)
1728        });
1729        let pcm: [i16; FRAME_SAMPLES] = match dq {
1730            Ok(Decoded::Voice(p)) => match vocoder.ambe_plus2_synth {
1731                AmbePlus2Synth::AmbePlus => ambe_plus2::synthesize_frame(&p, &mut vocoder.synth),
1732                AmbePlus2Synth::Baseline => {
1733                    let gamma_w = vocoder.synth.gamma_w;
1734                    synthesize_frame(&p, &err, gamma_w, &mut vocoder.synth)
1735                }
1736            },
1737            Ok(Decoded::Tone { params, .. }) => match vocoder.ambe_plus2_synth {
1738                AmbePlus2Synth::AmbePlus => ambe_plus2::synthesize_tone(&params, &mut vocoder.synth),
1739                AmbePlus2Synth::Baseline => {
1740                    let gamma_w = vocoder.synth.gamma_w;
1741                    synthesize_frame(&params, &err, gamma_w, &mut vocoder.synth)
1742                }
1743            },
1744            Ok(Decoded::Erasure) | Err(_) => [0i16; FRAME_SAMPLES],
1745        };
1746        let disposition = vocoder.synth.last_disposition();
1747        (
1748            pcm.to_vec(),
1749            DecodeStats {
1750                epsilon_0: stats_eps0,
1751                epsilon_t: stats_epst,
1752                disposition,
1753            },
1754        )
1755    }
1756
1757    fn pack_dibits_half(dibits: &[u8; 36]) -> [u8; 9] {
1758        let mut out = [0u8; 9];
1759        let mut bit = 0usize;
1760        for &d in dibits {
1761            for pos in (0..2).rev() {
1762                let b = (d >> pos) & 1;
1763                out[bit / 8] |= b << (7 - (bit % 8));
1764                bit += 1;
1765            }
1766        }
1767        out
1768    }
1769
1770    fn unpack_dibits_half(bytes: &[u8]) -> [u8; 36] {
1771        let mut out = [0u8; 36];
1772        let mut bit = 0usize;
1773        for slot in &mut out {
1774            let mut d = 0u8;
1775            for _ in 0..2 {
1776                let b = (bytes[bit / 8] >> (7 - (bit % 8))) & 1;
1777                d = (d << 1) | b;
1778                bit += 1;
1779            }
1780            *slot = d;
1781        }
1782        out
1783    }
1784
1785    fn pack_info_half(info: &[u16; 4]) -> [u8; 7] {
1786        let mut out = [0u8; 7];
1787        super::pack_info_bits(info, &INFO_WIDTHS, &mut out);
1788        out
1789    }
1790
1791    fn unpack_info_half(bytes: &[u8]) -> [u16; 4] {
1792        let mut out = [0u16; 4];
1793        super::unpack_info_bits(bytes, &INFO_WIDTHS, &mut out);
1794        out
1795    }
1796
1797    /// Wire-layer transcode: 9-byte FEC frame → 7-byte info-only
1798    /// frame. Pure bit-shuffling — no codec or predictor state.
1799    pub(super) fn fec_to_info_bytes(fec_bytes: &[u8]) -> [u8; 7] {
1800        let dibits = unpack_dibits_half(fec_bytes);
1801        let frame = decode_frame(&dibits);
1802        pack_info_half(&frame.info)
1803    }
1804
1805    /// Wire-layer transcode: 7-byte info-only frame → 9-byte FEC
1806    /// frame. Pure bit-shuffling — re-applies Golay/Hamming/PN.
1807    pub(super) fn info_to_fec_bytes(info_bytes: &[u8]) -> [u8; 9] {
1808        let info = unpack_info_half(info_bytes);
1809        let dibits = encode_frame(&info);
1810        pack_dibits_half(&dibits)
1811    }
1812}
1813
1814/// Pack an info-bit vector MSB-first into a byte buffer. Bit `k` of
1815/// `info[i]` is the high-order bit of that field; remaining bytes after
1816/// the last info bit are left at their initial value (callers pass a
1817/// zero-initialized slice when they want trailing pad bits to be zero).
1818fn pack_info_bits<const N: usize>(info: &[u16; N], widths: &[u8; N], out: &mut [u8]) {
1819    let mut bit_idx = 0usize;
1820    for i in 0..N {
1821        let w = widths[i] as usize;
1822        let v = info[i];
1823        for k in (0..w).rev() {
1824            let b = ((v >> k) & 1) as u8;
1825            out[bit_idx / 8] |= b << (7 - (bit_idx % 8));
1826            bit_idx += 1;
1827        }
1828    }
1829}
1830
1831fn unpack_info_bits<const N: usize>(bytes: &[u8], widths: &[u8; N], out: &mut [u16; N]) {
1832    let mut bit_idx = 0usize;
1833    for i in 0..N {
1834        let w = widths[i] as usize;
1835        let mut v = 0u16;
1836        for _ in 0..w {
1837            let b = (bytes[bit_idx / 8] >> (7 - (bit_idx % 8))) & 1;
1838            v = (v << 1) | u16::from(b);
1839            bit_idx += 1;
1840        }
1841        out[i] = v;
1842    }
1843}
1844
1845#[cfg(test)]
1846mod tests {
1847    use super::*;
1848
1849    fn periodic_pcm(period: usize, amplitude: i16) -> [i16; FRAME_SAMPLES] {
1850        let mut out = [0i16; FRAME_SAMPLES];
1851        for (n, slot) in out.iter_mut().enumerate() {
1852            let phase = (n % period) as f32 / period as f32;
1853            *slot = (amplitude as f32 * (2.0 * core::f32::consts::PI * phase).sin()) as i16;
1854        }
1855        out
1856    }
1857
1858    #[test]
1859    fn rate_byte_sizes_match_wire_layouts() {
1860        assert_eq!(Rate::Imbe7200x4400.fec_frame_bytes(), 18);
1861        assert_eq!(Rate::Imbe4400x4400.fec_frame_bytes(), 11);
1862        assert_eq!(Rate::AmbePlus2_3600x2450.fec_frame_bytes(), 9);
1863        assert_eq!(Rate::AmbePlus2_2450x2450.fec_frame_bytes(), 7);
1864        assert_eq!(Rate::Imbe7200x4400.frame_samples(), 160);
1865        assert_eq!(Rate::AmbePlus2_3600x2450.frame_samples(), 160);
1866    }
1867
1868    #[test]
1869    fn no_fec_imbe_roundtrip_smoke() {
1870        let mut tx = Vocoder::new(Rate::Imbe4400x4400);
1871        let mut rx = Vocoder::new(Rate::Imbe4400x4400);
1872        for _ in 0..5 {
1873            let pcm = periodic_pcm(40, 8000);
1874            let bits = tx.encode_pcm(&pcm).expect("encode");
1875            assert_eq!(bits.len(), 11, "no-FEC IMBE wire frame is 11 bytes (88 info bits)");
1876            let out = rx.decode_bits(&bits).expect("decode");
1877            assert_eq!(out.len(), FRAME_SAMPLES);
1878        }
1879    }
1880
1881    #[test]
1882    fn no_fec_ambeplus2_roundtrip_smoke() {
1883        let mut tx = Vocoder::new(Rate::AmbePlus2_2450x2450);
1884        let mut rx = Vocoder::new(Rate::AmbePlus2_2450x2450);
1885        for _ in 0..5 {
1886            let pcm = periodic_pcm(40, 6000);
1887            let bits = tx.encode_pcm(&pcm).expect("encode");
1888            assert_eq!(
1889                bits.len(),
1890                7,
1891                "no-FEC AMBE+2 wire frame is 7 bytes (49 info bits + 7 pad)"
1892            );
1893            let out = rx.decode_bits(&bits).expect("decode");
1894            assert_eq!(out.len(), FRAME_SAMPLES);
1895        }
1896    }
1897
1898    /// FEC and no-FEC variants should reach the same MbeParams (same
1899    /// codec underneath) — verified by encoding both ways and decoding
1900    /// the no-FEC bits, which must give identical PCM to a clean FEC
1901    /// roundtrip when no channel errors are injected.
1902    #[test]
1903    fn no_fec_full_matches_fec_full_on_clean_channel() {
1904        let mut tx_fec = Vocoder::new(Rate::Imbe7200x4400);
1905        let mut rx_fec = Vocoder::new(Rate::Imbe7200x4400);
1906        let mut tx_raw = Vocoder::new(Rate::Imbe4400x4400);
1907        let mut rx_raw = Vocoder::new(Rate::Imbe4400x4400);
1908        for k in 0..6 {
1909            let pcm = periodic_pcm(40 + k, 8000);
1910            let pcm_fec = rx_fec
1911                .decode_bits(&tx_fec.encode_pcm(&pcm).unwrap())
1912                .unwrap();
1913            let pcm_raw = rx_raw
1914                .decode_bits(&tx_raw.encode_pcm(&pcm).unwrap())
1915                .unwrap();
1916            assert_eq!(
1917                pcm_fec, pcm_raw,
1918                "no-FEC and FEC paths must match on a clean channel (frame {k})"
1919            );
1920        }
1921    }
1922
1923    #[test]
1924    fn no_fec_half_matches_fec_half_on_clean_channel() {
1925        let mut tx_fec = Vocoder::new(Rate::AmbePlus2_3600x2450);
1926        let mut rx_fec = Vocoder::new(Rate::AmbePlus2_3600x2450);
1927        let mut tx_raw = Vocoder::new(Rate::AmbePlus2_2450x2450);
1928        let mut rx_raw = Vocoder::new(Rate::AmbePlus2_2450x2450);
1929        for k in 0..6 {
1930            let pcm = periodic_pcm(40 + k, 6000);
1931            let pcm_fec = rx_fec
1932                .decode_bits(&tx_fec.encode_pcm(&pcm).unwrap())
1933                .unwrap();
1934            let pcm_raw = rx_raw
1935                .decode_bits(&tx_raw.encode_pcm(&pcm).unwrap())
1936                .unwrap();
1937            assert_eq!(
1938                pcm_fec, pcm_raw,
1939                "no-FEC and FEC paths must match on a clean channel (frame {k})"
1940            );
1941        }
1942    }
1943
1944    #[test]
1945    fn imbe_roundtrip_smoke() {
1946        let mut tx = Vocoder::new(Rate::Imbe7200x4400);
1947        let mut rx = Vocoder::new(Rate::Imbe7200x4400);
1948        // Three frames — first two are preroll on the analysis side
1949        // (return Silence dispatch), the third hits voice.
1950        for _ in 0..5 {
1951            let pcm = periodic_pcm(40, 8000);
1952            let bits = tx.encode_pcm(&pcm).expect("encode");
1953            assert_eq!(bits.len(), 18);
1954            let out = rx.decode_bits(&bits).expect("decode");
1955            assert_eq!(out.len(), FRAME_SAMPLES);
1956        }
1957        let stats = tx.last_stats();
1958        assert!(stats.analysis.is_some(), "encoder didn't fill stats");
1959    }
1960
1961    #[test]
1962    fn ambe_plus2_roundtrip_smoke() {
1963        let mut tx = Vocoder::new(Rate::AmbePlus2_3600x2450);
1964        let mut rx = Vocoder::new(Rate::AmbePlus2_3600x2450);
1965        for _ in 0..5 {
1966            let pcm = periodic_pcm(40, 8000);
1967            let bits = tx.encode_pcm(&pcm).expect("encode");
1968            assert_eq!(bits.len(), 9);
1969            let out = rx.decode_bits(&bits).expect("decode");
1970            assert_eq!(out.len(), FRAME_SAMPLES);
1971        }
1972        let stats = rx.last_stats();
1973        assert!(stats.decode.is_some(), "decoder didn't fill stats");
1974    }
1975
1976    #[test]
1977    fn wrong_pcm_length_errors() {
1978        let mut v = Vocoder::new(Rate::Imbe7200x4400);
1979        let r = v.encode_pcm(&[0i16; 159]);
1980        assert!(matches!(r, Err(VocoderError::WrongPcmLength { expected: 160, got: 159 })));
1981    }
1982
1983    #[test]
1984    fn wrong_bits_length_errors_per_rate() {
1985        let mut a = Vocoder::new(Rate::Imbe7200x4400);
1986        assert!(matches!(
1987            a.decode_bits(&[0u8; 9]),
1988            Err(VocoderError::WrongBitsLength { expected: 18, got: 9 })
1989        ));
1990        let mut b = Vocoder::new(Rate::AmbePlus2_3600x2450);
1991        assert!(matches!(
1992            b.decode_bits(&[0u8; 18]),
1993            Err(VocoderError::WrongBitsLength { expected: 9, got: 18 })
1994        ));
1995    }
1996
1997    #[test]
1998    fn reset_clears_state_and_keeps_rate() {
1999        let mut v = Vocoder::new(Rate::AmbePlus2_3600x2450);
2000        let pcm = periodic_pcm(40, 8000);
2001        let _ = v.encode_pcm(&pcm).unwrap();
2002        assert!(v.last_stats().analysis.is_some());
2003        v.reset();
2004        assert!(v.last_stats().analysis.is_none());
2005        assert_eq!(v.rate(), Rate::AmbePlus2_3600x2450);
2006    }
2007
2008    /// Diagnostic types round-trip through JSON when the `serde`
2009    /// feature is on. Validates that a future RPC layer
2010    /// (gRPC / protobuf / WS) can ship `FrameStats` over the wire
2011    /// without bespoke conversion.
2012    #[cfg(feature = "serde")]
2013    #[test]
2014    fn frame_stats_round_trip_through_json() {
2015        let mut v = Vocoder::new(Rate::Imbe7200x4400);
2016        let pcm = periodic_pcm(40, 8000);
2017        for _ in 0..3 {
2018            let bits = v.encode_pcm(&pcm).unwrap();
2019            let _ = v.decode_bits(&bits).unwrap();
2020        }
2021        let stats = v.last_stats().clone();
2022        let s = serde_json::to_string(&stats).expect("serialize FrameStats");
2023        let back: FrameStats = serde_json::from_str(&s).expect("deserialize FrameStats");
2024        // Round-trip equality on the parts we can compare without
2025        // full PartialEq derives — output kind + presence flags + the
2026        // one Copy-able sub-field.
2027        assert_eq!(stats.analysis.is_some(), back.analysis.is_some());
2028        assert_eq!(stats.decode.is_some(), back.decode.is_some());
2029        if let (Some(a), Some(b)) = (&stats.analysis, &back.analysis) {
2030            assert_eq!(a.output, b.output);
2031            assert_eq!(a.params, b.params);
2032        }
2033        if let (Some(a), Some(b)) = (&stats.decode, &back.decode) {
2034            assert_eq!(a.epsilon_0, b.epsilon_0);
2035            assert_eq!(a.epsilon_t, b.epsilon_t);
2036            assert_eq!(a.disposition, b.disposition);
2037        }
2038    }
2039
2040    /// `Rate` round-trips as a plain enum — the public-name variants
2041    /// stay stable across serde versions.
2042    #[cfg(feature = "serde")]
2043    #[test]
2044    fn rate_serializes_as_named_variant() {
2045        let s = serde_json::to_string(&Rate::Imbe7200x4400).unwrap();
2046        assert_eq!(s, "\"Imbe7200x4400\"");
2047        let back: Rate = serde_json::from_str(&s).unwrap();
2048        assert_eq!(back, Rate::Imbe7200x4400);
2049    }
2050
2051    /// Streaming encode iterator yields exactly `pcm.len() /
2052    /// frame_samples()` items and drops a trailing partial frame.
2053    #[test]
2054    fn encode_stream_yields_one_per_frame_drops_partial() {
2055        let mut v = Vocoder::new(Rate::Imbe7200x4400);
2056        // 5 full frames + 50 trailing samples (partial — should drop).
2057        let mut pcm: Vec<i16> = Vec::with_capacity(5 * FRAME_SAMPLES + 50);
2058        for f in 0..5 {
2059            pcm.extend_from_slice(&periodic_pcm(40, (1000 + f * 100) as i16));
2060        }
2061        pcm.extend(std::iter::repeat(0i16).take(50));
2062        let stream = v.encode_stream(&pcm);
2063        assert_eq!(stream.len(), 5);
2064        let bits: Vec<Vec<u8>> = stream.collect::<Result<Vec<_>, _>>().unwrap();
2065        assert_eq!(bits.len(), 5);
2066        for b in &bits {
2067            assert_eq!(b.len(), 18); // Imbe7200x4400 FEC frame size
2068        }
2069    }
2070
2071    /// Streaming decode parallels encode: one item per FEC frame,
2072    /// trailing partial bytes dropped, output 160 samples each.
2073    #[test]
2074    fn decode_stream_yields_one_per_frame_drops_partial() {
2075        let mut tx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2076        let mut pcm: Vec<i16> = Vec::with_capacity(7 * FRAME_SAMPLES);
2077        for _ in 0..7 {
2078            pcm.extend_from_slice(&periodic_pcm(40, 5000));
2079        }
2080        let bits: Vec<u8> = tx
2081            .encode_stream(&pcm)
2082            .collect::<Result<Vec<_>, _>>()
2083            .unwrap()
2084            .into_iter()
2085            .flatten()
2086            .collect();
2087        // Append 4 stray bytes that should be dropped.
2088        let mut padded = bits.clone();
2089        padded.extend_from_slice(&[0; 4]);
2090
2091        let mut rx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2092        let frames: Vec<Vec<i16>> = rx
2093            .decode_stream(&padded)
2094            .collect::<Result<Vec<_>, _>>()
2095            .unwrap();
2096        assert_eq!(frames.len(), 7);
2097        for f in &frames {
2098            assert_eq!(f.len(), FRAME_SAMPLES);
2099        }
2100    }
2101
2102    /// `DecodeStats::disposition` populates after a decode call.
2103    /// On clean own-encoded bits the disposition is `Use`; on bits
2104    /// crafted to cross the mute threshold (high ε_t injected by
2105    /// pre-loading `state.epsilon_r`) it should be `Mute`.
2106    #[test]
2107    fn decode_stats_carry_disposition() {
2108        let mut tx = Vocoder::new(Rate::Imbe7200x4400);
2109        let mut rx = Vocoder::new(Rate::Imbe7200x4400);
2110        for _ in 0..5 {
2111            let pcm = periodic_pcm(40, 8000);
2112            let bits = tx.encode_pcm(&pcm).unwrap();
2113            let _ = rx.decode_bits(&bits).unwrap();
2114        }
2115        let disp = rx
2116            .last_stats()
2117            .decode
2118            .as_ref()
2119            .expect("decode stats populated after decode call")
2120            .disposition
2121            .expect("disposition surfaced");
2122        // Clean own-encoded bits: 0 errors per frame, so Use.
2123        assert_eq!(disp, FrameDisposition::Use);
2124        assert_eq!(rx.last_disposition(), Some(FrameDisposition::Use));
2125    }
2126
2127    /// Pure-sine input at an Annex T frequency, with tone detection
2128    /// enabled, produces a tone frame instead of voice. The decoder
2129    /// recognises the tone-frame signature and reconstructs the
2130    /// matching MBE params.
2131    #[test]
2132    fn tone_detection_emits_tone_frame_and_decoder_recognises_it() {
2133        // Half-rate is the only rate with tone-frame signaling.
2134        let mut tx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2135        tx.set_tone_detection(true);
2136        // Annex T id=10 → 312.5 Hz. Generate one full frame of clean
2137        // sine at i16 amplitude 8000.
2138        let mut pcm = [0i16; FRAME_SAMPLES];
2139        let two_pi = 2.0 * core::f64::consts::PI;
2140        for (n, slot) in pcm.iter_mut().enumerate() {
2141            let s = 8000.0_f64 * (two_pi * 312.5 * n as f64 / 8000.0).sin();
2142            *slot = s.round() as i16;
2143        }
2144        let bits = tx.encode_pcm(&pcm).unwrap();
2145        assert_eq!(bits.len(), 9);
2146
2147        // Confirm the encoder reported a Tone output.
2148        match tx.last_stats().analysis.as_ref().unwrap().output {
2149            AnalysisOutputKind::Tone { id, amplitude: _ } => assert_eq!(id, 10),
2150            other => panic!("expected Tone, got {other:?}"),
2151        }
2152
2153        // Decoder side: parse the bits and confirm it classifies as
2154        // a tone frame (FrameKind::Tone via the §2.10.1 signature
2155        // dispatch).
2156        use crate::ambe_plus2_wire::dequantize::{FrameKind, classify_ambe_plus2_frame};
2157        use crate::ambe_plus2_wire::frame::decode_frame;
2158        let mut dibits = [0u8; 36];
2159        let mut bit = 0;
2160        for slot in &mut dibits {
2161            let mut d = 0u8;
2162            for _ in 0..2 {
2163                let b = (bits[bit / 8] >> (7 - (bit % 8))) & 1;
2164                d = (d << 1) | b;
2165                bit += 1;
2166            }
2167            *slot = d;
2168        }
2169        let frame = decode_frame(&dibits);
2170        assert_eq!(classify_ambe_plus2_frame(&frame.info), FrameKind::Tone);
2171
2172        // Decoding through the Vocoder yields PCM (synthesize_tone
2173        // path); we don't assert frequency parity because tone-synth
2174        // calibration depends on §1.10/§11 amplitude scaling that's
2175        // separately tracked, but the output should be non-trivial.
2176        let mut rx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2177        let out = rx.decode_bits(&bits).unwrap();
2178        assert_eq!(out.len(), FRAME_SAMPLES);
2179    }
2180
2181    /// With tone-detection off (default), the same pure-sine input
2182    /// goes through the voice analysis pipeline and produces a
2183    /// regular voice frame.
2184    #[test]
2185    fn tone_detection_off_means_voice_path_even_for_pure_sine() {
2186        let mut tx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2187        // (default) tone_detection == false
2188        let mut pcm = [0i16; FRAME_SAMPLES];
2189        let two_pi = 2.0 * core::f64::consts::PI;
2190        for (n, slot) in pcm.iter_mut().enumerate() {
2191            let s = 8000.0_f64 * (two_pi * 312.5 * n as f64 / 8000.0).sin();
2192            *slot = s.round() as i16;
2193        }
2194        let _ = tx.encode_pcm(&pcm).unwrap();
2195        let stats = tx.last_stats().analysis.as_ref().unwrap();
2196        assert!(matches!(
2197            stats.output,
2198            AnalysisOutputKind::Voice | AnalysisOutputKind::Silence
2199        ));
2200        assert!(!matches!(stats.output, AnalysisOutputKind::Tone { .. }));
2201        assert!(stats.tone_detect.is_none());
2202    }
2203
2204    /// On full-rate IMBE, tone detection is **detection-only**: the
2205    /// wire frame stays a regular voice frame (Phase 1 has no
2206    /// tone-frame opcode at the codec layer per BABA-A) but the
2207    /// detected `(I_D, A_D)` is surfaced via `tone_detect` so
2208    /// application-layer signaling (LCW, paging) can route on it.
2209    #[test]
2210    fn tone_detection_on_phase1_surfaces_metadata_without_changing_wire() {
2211        let mut tx = Vocoder::new(Rate::Imbe7200x4400);
2212        tx.set_tone_detection(true);
2213        let mut pcm = [0i16; FRAME_SAMPLES];
2214        let two_pi = 2.0 * core::f64::consts::PI;
2215        for (n, slot) in pcm.iter_mut().enumerate() {
2216            let s = 8000.0_f64 * (two_pi * 312.5 * n as f64 / 8000.0).sin();
2217            *slot = s.round() as i16;
2218        }
2219        let bits_with_detect = tx.encode_pcm(&pcm).unwrap();
2220        let stats = tx.last_stats().analysis.as_ref().unwrap();
2221        // Wire output is still voice (no Phase 1 tone-frame opcode).
2222        assert!(matches!(
2223            stats.output,
2224            AnalysisOutputKind::Voice | AnalysisOutputKind::Silence
2225        ));
2226        // But the detector populated tone_detect with the matching id.
2227        let det = stats.tone_detect.expect("Phase 1 detection metadata");
2228        assert_eq!(det.id, 10); // Annex T id=10 → 312.5 Hz
2229
2230        // Bit-exact equivalence with detection off — running the
2231        // detector must not perturb the analysis encoder state.
2232        let mut tx_off = Vocoder::new(Rate::Imbe7200x4400);
2233        let bits_no_detect = tx_off.encode_pcm(&pcm).unwrap();
2234        assert_eq!(bits_with_detect, bits_no_detect);
2235    }
2236
2237    /// `LiveEncoder` accepts arbitrary chunk sizes, holds residue
2238    /// across `push` calls, and emits the same bits as a one-shot
2239    /// `Vocoder::encode_pcm` per-frame loop.
2240    #[test]
2241    fn live_encoder_handles_arbitrary_chunk_sizes() {
2242        let mut total_pcm: Vec<i16> = Vec::with_capacity(7 * FRAME_SAMPLES);
2243        for _ in 0..7 {
2244            total_pcm.extend_from_slice(&periodic_pcm(40, 6000));
2245        }
2246        // Reference: per-frame Vocoder loop on the same input.
2247        let mut ref_v = Vocoder::new(Rate::Imbe7200x4400);
2248        let mut ref_bits: Vec<u8> = Vec::new();
2249        for chunk in total_pcm.chunks_exact(FRAME_SAMPLES) {
2250            ref_bits.extend(ref_v.encode_pcm(chunk).unwrap());
2251        }
2252        // Live: feed in mismatched chunk sizes (250, 50, 333, rest).
2253        let mut live = LiveEncoder::new(Rate::Imbe7200x4400);
2254        let mut live_bits: Vec<u8> = Vec::new();
2255        let splits = [250usize, 50, 333];
2256        let mut pos = 0;
2257        for &n in &splits {
2258            let end = (pos + n).min(total_pcm.len());
2259            for r in live.push(&total_pcm[pos..end]) {
2260                live_bits.extend(r.unwrap());
2261            }
2262            pos = end;
2263        }
2264        for r in live.push(&total_pcm[pos..]) {
2265            live_bits.extend(r.unwrap());
2266        }
2267        assert_eq!(live_bits, ref_bits);
2268        // Total samples 1120 = 7 frames exactly, so no residue.
2269        assert_eq!(live.pending_samples(), 0);
2270    }
2271
2272    /// `LiveEncoder` correctly retains residue when input ends
2273    /// mid-frame.
2274    #[test]
2275    fn live_encoder_residue_held_across_calls() {
2276        let mut live = LiveEncoder::new(Rate::Imbe7200x4400);
2277        // 1.5 frames of input split into two pushes.
2278        let pcm: Vec<i16> = periodic_pcm(40, 6000)
2279            .iter()
2280            .copied()
2281            .chain(periodic_pcm(40, 6000)[..80].iter().copied())
2282            .collect();
2283        assert_eq!(pcm.len(), 240);
2284        let frames = live.push(&pcm[..120]);
2285        assert!(frames.is_empty());
2286        assert_eq!(live.pending_samples(), 120);
2287        let frames = live.push(&pcm[120..]);
2288        assert_eq!(frames.len(), 1);
2289        assert!(frames[0].is_ok());
2290        assert_eq!(live.pending_samples(), 80);
2291    }
2292
2293    /// `LiveDecoder` handles arbitrary chunk sizes for the byte stream
2294    /// and matches a per-frame `Vocoder::decode_bits` loop.
2295    #[test]
2296    fn live_decoder_handles_arbitrary_chunk_sizes() {
2297        let mut tx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2298        let mut all_pcm: Vec<i16> = Vec::with_capacity(5 * FRAME_SAMPLES);
2299        for _ in 0..5 {
2300            all_pcm.extend_from_slice(&periodic_pcm(40, 5000));
2301        }
2302        let bits: Vec<u8> = tx
2303            .encode_stream(&all_pcm)
2304            .collect::<Result<Vec<_>, _>>()
2305            .unwrap()
2306            .into_iter()
2307            .flatten()
2308            .collect();
2309        let mut ref_v = Vocoder::new(Rate::AmbePlus2_3600x2450);
2310        let mut ref_pcm: Vec<i16> = Vec::new();
2311        for chunk in bits.chunks_exact(9) {
2312            ref_pcm.extend(ref_v.decode_bits(chunk).unwrap());
2313        }
2314        let mut live = LiveDecoder::new(Rate::AmbePlus2_3600x2450);
2315        let mut live_pcm: Vec<i16> = Vec::new();
2316        // Feed in 7-byte chunks (less than one frame each).
2317        for chunk in bits.chunks(7) {
2318            for r in live.push(chunk) {
2319                live_pcm.extend(r.unwrap());
2320            }
2321        }
2322        assert_eq!(live_pcm, ref_pcm);
2323        assert_eq!(live.pending_bytes(), 0); // 5 frames × 9 bytes = 45, multiple of 7? No (45 = 6*7+3) — pending should be 3 bytes if 5 frames don't fully drain. Wait, 5 frames take 45 bytes, fed as 7 chunks of 7 + 1 chunk of 2 = 45 bytes total. After feeding all bytes, all 5 frames produced. pending = 0.
2324    }
2325
2326    /// `discard_pending` drops residue without emitting partial output.
2327    #[test]
2328    fn live_encoder_discard_pending_clears_residue() {
2329        let mut live = LiveEncoder::new(Rate::Imbe7200x4400);
2330        let pcm = periodic_pcm(40, 6000);
2331        let frames = live.push(&pcm[..80]);
2332        assert!(frames.is_empty());
2333        assert_eq!(live.pending_samples(), 80);
2334        live.discard_pending();
2335        assert_eq!(live.pending_samples(), 0);
2336    }
2337
2338    /// Transcoder bridges P25 Phase 1 ↔ Phase 2 at the FEC-byte
2339    /// boundary. State advances per call; rates are validated.
2340    #[test]
2341    fn transcoder_phase1_to_phase2_changes_frame_size() {
2342        let mut tx = Transcoder::new(Rate::Imbe7200x4400, Rate::AmbePlus2_3600x2450).unwrap();
2343        assert_eq!(
2344            tx.direction(),
2345            TranscodeDirection::new(Rate::Imbe7200x4400, Rate::AmbePlus2_3600x2450)
2346        );
2347        // Encode some Phase 1 frames first to get realistic bits.
2348        let mut enc = Vocoder::new(Rate::Imbe7200x4400);
2349        let pcm = periodic_pcm(40, 6000);
2350        for _ in 0..3 {
2351            let phase1 = enc.encode_pcm(&pcm).unwrap();
2352            assert_eq!(phase1.len(), 18);
2353            let phase2 = tx.transcode(&phase1).unwrap();
2354            assert_eq!(phase2.len(), 9, "P1→P2 transcode produces 9-byte frames");
2355        }
2356    }
2357
2358    #[test]
2359    fn transcoder_phase2_to_phase1_changes_frame_size() {
2360        let mut tx = Transcoder::new(Rate::AmbePlus2_3600x2450, Rate::Imbe7200x4400).unwrap();
2361        let mut enc = Vocoder::new(Rate::AmbePlus2_3600x2450);
2362        let pcm = periodic_pcm(40, 6000);
2363        for _ in 0..3 {
2364            let phase2 = enc.encode_pcm(&pcm).unwrap();
2365            assert_eq!(phase2.len(), 9);
2366            let phase1 = tx.transcode(&phase2).unwrap();
2367            assert_eq!(phase1.len(), 18);
2368        }
2369    }
2370
2371    #[test]
2372    fn transcoder_rejects_wrong_input_length() {
2373        let mut tx = Transcoder::new(Rate::Imbe7200x4400, Rate::AmbePlus2_3600x2450).unwrap();
2374        assert!(matches!(
2375            tx.transcode(&[0u8; 9]),
2376            Err(VocoderError::WrongBitsLength { expected: 18, got: 9 })
2377        ));
2378    }
2379
2380    /// Unsupported `(from, to)` pair surfaces as `UnsupportedTranscode`
2381    /// rather than a panic.
2382    #[test]
2383    fn transcoder_rejects_unsupported_pair() {
2384        let res = Transcoder::new(Rate::Imbe7200x4400, Rate::Imbe7200x4400);
2385        assert!(matches!(
2386            res.err(),
2387            Some(VocoderError::UnsupportedTranscode {
2388                from: Rate::Imbe7200x4400,
2389                to: Rate::Imbe7200x4400,
2390            })
2391        ));
2392    }
2393
2394    /// Same-codec FEC ↔ no-FEC transcode is lossless: strip then add
2395    /// (or add then strip) reproduces the original bytes exactly.
2396    #[test]
2397    fn transcoder_full_fec_to_info_roundtrip_is_lossless() {
2398        let mut enc = Vocoder::new(Rate::Imbe7200x4400);
2399        let mut strip =
2400            Transcoder::new(Rate::Imbe7200x4400, Rate::Imbe4400x4400).unwrap();
2401        let mut add =
2402            Transcoder::new(Rate::Imbe4400x4400, Rate::Imbe7200x4400).unwrap();
2403        for k in 0..4 {
2404            let pcm = periodic_pcm(40 + k, 7000);
2405            let fec = enc.encode_pcm(&pcm).unwrap();
2406            assert_eq!(fec.len(), 18);
2407            let info = strip.transcode(&fec).unwrap();
2408            assert_eq!(info.len(), 11);
2409            let fec2 = add.transcode(&info).unwrap();
2410            assert_eq!(fec2, fec, "FEC strip + add round-trips byte-for-byte");
2411        }
2412    }
2413
2414    #[test]
2415    fn transcoder_half_fec_to_info_roundtrip_is_lossless() {
2416        let mut enc = Vocoder::new(Rate::AmbePlus2_3600x2450);
2417        let mut strip =
2418            Transcoder::new(Rate::AmbePlus2_3600x2450, Rate::AmbePlus2_2450x2450).unwrap();
2419        let mut add =
2420            Transcoder::new(Rate::AmbePlus2_2450x2450, Rate::AmbePlus2_3600x2450).unwrap();
2421        for k in 0..4 {
2422            let pcm = periodic_pcm(40 + k, 6000);
2423            let fec = enc.encode_pcm(&pcm).unwrap();
2424            assert_eq!(fec.len(), 9);
2425            let info = strip.transcode(&fec).unwrap();
2426            assert_eq!(info.len(), 7);
2427            let fec2 = add.transcode(&info).unwrap();
2428            assert_eq!(fec2, fec, "FEC strip + add round-trips byte-for-byte");
2429        }
2430    }
2431
2432    /// `extract_params` runs the analysis encoder on PCM and returns
2433    /// MbeParams. Multiple calls advance state correctly (preroll
2434    /// → voice transition).
2435    #[test]
2436    fn extract_params_returns_params_per_frame() {
2437        let mut v = Vocoder::new(Rate::Imbe7200x4400);
2438        // First few frames are preroll; analysis should still return
2439        // silence params, not error.
2440        let pcm = periodic_pcm(40, 6000);
2441        let p1 = v.extract_params(&pcm).unwrap();
2442        let p2 = v.extract_params(&pcm).unwrap();
2443        let p3 = v.extract_params(&pcm).unwrap();
2444        // Preroll dispatches silence; by frame 3+ we should see voice
2445        // params (non-silence ω₀ or non-zero amplitudes).
2446        let any_voice = [&p1, &p2, &p3].iter().any(|p| {
2447            let amps = p.amplitudes_slice();
2448            amps.iter().any(|&a| a > 0.0)
2449        });
2450        assert!(any_voice, "no voice params after 3 frames of periodic PCM");
2451    }
2452
2453    /// extract_params + synthesize_params chain together — extract
2454    /// params from PCM, immediately synthesize them back to PCM, get
2455    /// non-trivial output. (Not the same as the input — that would
2456    /// require the wire FEC chain to advance the synth predictor in
2457    /// step with the analysis predictor — but the synth output is
2458    /// well-defined.)
2459    #[test]
2460    fn extract_then_synthesize_roundtrips_through_params() {
2461        let mut a = Vocoder::new(Rate::Imbe7200x4400);
2462        let mut b = Vocoder::new(Rate::Imbe7200x4400);
2463        let pcm = periodic_pcm(40, 6000);
2464        for _ in 0..5 {
2465            let params = a.extract_params(&pcm).unwrap();
2466            let resynth = b.synthesize_params(&params);
2467            assert_eq!(resynth.len(), FRAME_SAMPLES);
2468        }
2469    }
2470
2471    /// `synthesize_params` accepts arbitrary MbeParams and produces a
2472    /// 160-sample PCM frame. Round-trips cleanly with `MbeParams::silence`
2473    /// (full-rate) and `MbeParams::silence_ambe_plus2` (half-rate) —
2474    /// silence params synthesize to (near-)silent output.
2475    #[test]
2476    fn synthesize_params_emits_one_frame_per_call() {
2477        let mut tx = Vocoder::new(Rate::Imbe7200x4400);
2478        let pcm = tx.synthesize_params(&MbeParams::silence());
2479        assert_eq!(pcm.len(), FRAME_SAMPLES);
2480        // Silence params → low-amplitude output.
2481        let peak = pcm.iter().map(|&s| s.unsigned_abs()).max().unwrap_or(0);
2482        assert!(peak < 5000, "silence params produced peak={peak}");
2483
2484        let mut rx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2485        let pcm = rx.synthesize_params(&MbeParams::silence_ambe_plus2());
2486        assert_eq!(pcm.len(), FRAME_SAMPLES);
2487        let peak = pcm.iter().map(|&s| s.unsigned_abs()).max().unwrap_or(0);
2488        assert!(peak < 5000);
2489    }
2490
2491    /// `synthesize_params` advances synth state so a follow-up
2492    /// `last_disposition` reflects the most-recent frame.
2493    #[test]
2494    fn synthesize_params_advances_synth_disposition() {
2495        let mut v = Vocoder::new(Rate::Imbe7200x4400);
2496        assert_eq!(v.last_disposition(), None);
2497        let _ = v.synthesize_params(&MbeParams::silence());
2498        // Clean params + zero error context → Use.
2499        assert_eq!(v.last_disposition(), Some(FrameDisposition::Use));
2500    }
2501
2502    /// Builder applies all five config knobs in one expression.
2503    #[test]
2504    fn builder_configures_all_knobs() {
2505        let v = Vocoder::builder(Rate::AmbePlus2_3600x2450)
2506            .tone_detection(true)
2507            .repeat_reset_after(Some(3))
2508            .silence_dispatch(true)
2509            .pitch_silence_override(true)
2510            .ambe_plus2_synth(AmbePlus2Synth::Baseline)
2511            .build();
2512        assert_eq!(v.rate(), Rate::AmbePlus2_3600x2450);
2513        assert!(v.tone_detection());
2514        assert_eq!(v.repeat_reset_after(), Some(3));
2515        assert!(v.silence_dispatch());
2516        assert!(v.pitch_silence_override());
2517        assert_eq!(v.ambe_plus2_synth(), AmbePlus2Synth::Baseline);
2518    }
2519
2520    /// Half-rate synth choice routes to the right backend. Both
2521    /// modes produce non-trivial audio. They MAY converge to
2522    /// identical PCM on simple/periodic inputs (the AMBE+ phase
2523    /// regen is a no-op when all bands voiced cleanly), so the
2524    /// assertion is non-silent + correct rate, not bit-difference.
2525    #[test]
2526    fn ambe_plus2_synth_modes_both_decode_cleanly() {
2527        let mut tx = Vocoder::new(Rate::AmbePlus2_3600x2450);
2528        let pcm = periodic_pcm(40, 6000);
2529        let mut bits_buf: Vec<u8> = Vec::new();
2530        for _ in 0..5 {
2531            bits_buf.extend(tx.encode_pcm(&pcm).unwrap());
2532        }
2533
2534        for gen in [AmbePlus2Synth::AmbePlus, AmbePlus2Synth::Baseline] {
2535            let mut rx = Vocoder::builder(Rate::AmbePlus2_3600x2450)
2536                .ambe_plus2_synth(gen)
2537                .build();
2538            assert_eq!(rx.ambe_plus2_synth(), gen);
2539            let mut out_pcm: Vec<i16> = Vec::new();
2540            for chunk in bits_buf.chunks_exact(9) {
2541                out_pcm.extend(rx.decode_bits(chunk).unwrap());
2542            }
2543            let rms = (out_pcm.iter().map(|&s| (s as f64) * (s as f64)).sum::<f64>()
2544                / out_pcm.len() as f64).sqrt();
2545            assert!(rms > 50.0, "{gen:?} output too quiet: rms={rms}");
2546        }
2547    }
2548
2549    /// Default builder = spec-faithful (no beyond-spec or opt-in knobs).
2550    #[test]
2551    fn builder_defaults_match_vocoder_new() {
2552        let a = Vocoder::builder(Rate::Imbe7200x4400).build();
2553        let b = Vocoder::new(Rate::Imbe7200x4400);
2554        assert_eq!(a.rate(), b.rate());
2555        assert_eq!(a.tone_detection(), b.tone_detection());
2556        assert_eq!(a.repeat_reset_after(), b.repeat_reset_after());
2557        assert_eq!(a.silence_dispatch(), b.silence_dispatch());
2558        assert_eq!(a.pitch_silence_override(), b.pitch_silence_override());
2559        assert_eq!(a.ambe_plus2_synth(), b.ambe_plus2_synth());
2560        assert!(!a.tone_detection());
2561        assert!(a.repeat_reset_after().is_none());
2562        assert!(!a.silence_dispatch());
2563        assert!(!a.pitch_silence_override());
2564        assert_eq!(a.ambe_plus2_synth(), AmbePlus2Synth::AmbePlus);
2565    }
2566
2567    /// `flush` zero-pads residue and emits one final frame; on an
2568    /// empty buffer it's a no-op returning `Ok(None)`.
2569    #[test]
2570    fn live_encoder_flush_emits_padded_residue() {
2571        let mut live = LiveEncoder::new(Rate::Imbe7200x4400);
2572        // Empty buffer → flush is a no-op.
2573        assert!(matches!(live.flush(), Ok(None)));
2574
2575        // Half-frame residue → flush emits one frame.
2576        let pcm = periodic_pcm(40, 6000);
2577        let _ = live.push(&pcm[..80]);
2578        assert_eq!(live.pending_samples(), 80);
2579        let tail = live.flush().unwrap().expect("residue → frame");
2580        assert_eq!(tail.len(), 18);
2581        assert_eq!(live.pending_samples(), 0);
2582
2583        // Subsequent flush is a no-op (buffer drained).
2584        assert!(matches!(live.flush(), Ok(None)));
2585    }
2586
2587    /// `reset` clears both vocoder state and the residue buffer.
2588    #[test]
2589    fn live_encoder_reset_clears_everything() {
2590        let mut live = LiveEncoder::new(Rate::AmbePlus2_3600x2450);
2591        let pcm = periodic_pcm(40, 5000);
2592        let _ = live.push(&pcm[..120]);
2593        assert_eq!(live.pending_samples(), 120);
2594        let _ = live.vocoder().last_stats();
2595        live.reset();
2596        assert_eq!(live.pending_samples(), 0);
2597        assert!(live.vocoder().last_stats().analysis.is_none());
2598        assert_eq!(live.rate(), Rate::AmbePlus2_3600x2450);
2599    }
2600
2601    /// Streaming and per-frame paths produce identical output —
2602    /// the iterator is a thin chunking wrapper around `encode_pcm`,
2603    /// state advances the same way.
2604    #[test]
2605    fn encode_stream_matches_per_frame_calls_byte_for_byte() {
2606        let mut a = Vocoder::new(Rate::Imbe7200x4400);
2607        let mut b = Vocoder::new(Rate::Imbe7200x4400);
2608        let mut pcm: Vec<i16> = Vec::with_capacity(4 * FRAME_SAMPLES);
2609        for _ in 0..4 {
2610            pcm.extend_from_slice(&periodic_pcm(40, 6000));
2611        }
2612        let by_stream: Vec<u8> = a
2613            .encode_stream(&pcm)
2614            .collect::<Result<Vec<_>, _>>()
2615            .unwrap()
2616            .into_iter()
2617            .flatten()
2618            .collect();
2619        let mut by_call: Vec<u8> = Vec::new();
2620        for chunk in pcm.chunks_exact(FRAME_SAMPLES) {
2621            by_call.extend(b.encode_pcm(chunk).unwrap());
2622        }
2623        assert_eq!(by_stream, by_call);
2624    }
2625}