pub struct Vocoder { /* private fields */ }Expand description
Chip-shaped façade over the rate-specific encoder + decoder + synth pipelines.
Owns all per-rate state internally (analysis, decoder, synth). One
Vocoder is both encoder and decoder for a single channel
direction; consumers running bidirectional voice typically allocate
two — one each direction — to mirror the chip’s per-direction
state isolation.
State is not Sync; one channel = one thread. State is Send,
so the channel can move between threads.
Implementations§
Source§impl Vocoder
impl Vocoder
Sourcepub fn new(rate: Rate) -> Self
pub fn new(rate: Rate) -> Self
Open a new channel at the given rate, all state cold.
The unvoiced noise generator is selected per-rate: IMBE full-rate uses the BABA-A §1.12.1 spec LCG (171/11213/53125, seed 3147) because the DVSI chip’s full-rate path matches that recurrence; AMBE+2 half-rate uses the chip-empirical LCG (173/13849/65536, seed 60584) — see gap report 0025. Probe 1 confirmed 0.945 sample-level correlation against the live chip on the half-rate path.
Sourcepub fn set_ambe_plus2_synth(&mut self, gen: AmbePlus2Synth)
pub fn set_ambe_plus2_synth(&mut self, gen: AmbePlus2Synth)
Configure which half-rate synth flavor Self::decode_bits +
Self::synthesize_params use on Phase 2 / AMBE+2 input.
No-op for full-rate (which always uses the BABA-A §1.12
baseline IMBE synth). Default AmbePlus2Synth::AmbePlus.
Sourcepub fn ambe_plus2_synth(&self) -> AmbePlus2Synth
pub fn ambe_plus2_synth(&self) -> AmbePlus2Synth
Currently configured half-rate synth flavor.
Sourcepub fn set_tone_detection(&mut self, enabled: bool)
pub fn set_tone_detection(&mut self, enabled: bool)
Enable encode-side Annex T tone detection. Each PCM frame is
inspected for a clean single-frequency tone or DTMF / Knox
pair matching an Annex T entry; on a hit the detection result
is surfaced via AnalysisStats::tone_detect. Default off.
Wire behavior is rate-dependent:
- Half-rate (AMBE+2 Phase 2): on a hit, the channel emits
an Annex T tone frame instead of running the voice analysis
pipeline;
AnalysisStats::outputisTone { id, amplitude }andAnalysisStats::tone_detectmirrors the same values. - Full-rate (IMBE Phase 1): the wire frame remains a
regular voice / silence frame because P25 Phase 1 has no
tone-frame opcode at the codec layer (BABA-A; spec-author
confirmation 2026-04-30).
AnalysisStats::tone_detectis still populated so application-layer consumers can route(I_D, A_D)to LCW, paging, or other out-of-band signaling per BABA-A§5.4.
Spec context: BABA-A §2.10 defines the tone-frame payload
(Annex T table + signature bits) but the spec leaves “is this
a tone?” to the implementer (per §0.0.1 of the impl spec).
This is a DSP design choice, not a P25-IP question.
Sourcepub fn tone_detection(&self) -> bool
pub fn tone_detection(&self) -> bool
Whether encode-side tone detection is currently enabled.
Sourcepub fn set_repeat_reset_after(&mut self, n: Option<u32>)
pub fn set_repeat_reset_after(&mut self, n: Option<u32>)
Configure the beyond-spec consecutive-repeat reset threshold
on the synth side. After n consecutive Repeat/Mute frames,
substitution falls back to a default-fundamental + amps=1.0
frame instead of replaying the prior last_good. None
(default) is the spec-faithful path per gap 0022 resolution.
JMBE / SDRTrunk use Some(3) for chip-stream interop quality.
Sourcepub fn repeat_reset_after(&self) -> Option<u32>
pub fn repeat_reset_after(&self) -> Option<u32>
Current consecutive-repeat reset threshold (None = disabled,
spec-faithful).
Sourcepub fn set_chip_compat(&mut self, on: bool)
pub fn set_chip_compat(&mut self, on: bool)
Enable JMBE-style error-rate freeze on Repeat (beyond-spec; gap
0021). When enabled, frames whose disposition is Repeat do
not advance ε_R; the previous frame’s value is carried
forward. This mirrors JMBE’s
IMBEModelParameters.copy() calling
setErrorRate(previous.getErrorRate()) and prevents runs of
high-error chip-encoded frames from driving ε_R past the
0.0875 Mute threshold. Default false is the spec-faithful
path. Enable for decoding chip-encoded P25 air traffic; leave
off for our-encoder → our-decoder loops and spec-conformance
testing.
Sourcepub fn chip_compat(&self) -> bool
pub fn chip_compat(&self) -> bool
Current chip-compat (error-rate freeze on Repeat) setting.
Sourcepub fn set_chip_compat_spectral_clamp(&mut self, on: bool)
pub fn set_chip_compat_spectral_clamp(&mut self, on: bool)
Force the beyond-spec spectral-discontinuity clamp on (gap 0026)
independently of Self::set_chip_compat. Most consumers
should leave this off — the umbrella chip_compat flag already
auto-enables the clamp alongside the gap-0021 ε_R freeze.
This standalone toggle exists for the narrow case of wanting
the clamp without the ε_R freeze. With either flag enabled,
frames whose harmonic count L jumps by more than 5 from the
previous frame (up-jumps only) or whose err.epsilon_0 ≥ 2
have their post-smoothing amplitudes scaled by 0.73 for one
frame. Mirrors observed DVSI AMBE-3000R behavior on pitch/L
jumps and Repeat frames; reduces audible “scratch” on noisy
Phase 2 traffic where Golay-corrected frames decode to
spectrally-distant params from their neighbors. Default
false keeps the spec-faithful path.
Sourcepub fn chip_compat_spectral_clamp(&self) -> bool
pub fn chip_compat_spectral_clamp(&self) -> bool
Current standalone spectral-discontinuity clamp setting.
Note: the clamp also fires whenever Self::chip_compat is
true, regardless of this setting.
Sourcepub fn set_silence_dispatch(&mut self, on: bool)
pub fn set_silence_dispatch(&mut self, on: bool)
Enable the §0.8.4 silence-dispatch path on the analysis encoder.
When on, frames whose energy clears the silence detector’s
hysteresis emit AnalysisOutput::Silence (rate-appropriate
silence params) instead of running the full pitch / V-UV /
amplitude pipeline. Default off — pass-through per addendum
§0.8.8 recommendation.
Sourcepub fn silence_dispatch(&self) -> bool
pub fn silence_dispatch(&self) -> bool
Whether silence dispatch is currently enabled.
Sourcepub fn set_pitch_silence_override(&mut self, on: bool)
pub fn set_pitch_silence_override(&mut self, on: bool)
Enable the joint-signal silence override — dispatches
pitch-unreliable low-energy frames as silence even if they
don’t reach the §0.8.4 hysteresis threshold. Requires
Self::set_silence_dispatch also be on. Default off.
Sourcepub fn pitch_silence_override(&self) -> bool
pub fn pitch_silence_override(&self) -> bool
Whether the joint-signal silence override is currently enabled.
Sourcepub fn set_default_pitch_on_silence(&mut self, on: bool)
pub fn set_default_pitch_on_silence(&mut self, on: bool)
Enable or disable the onset-attack mitigation. On near-silent
frames the encoder commits a short default pitch
(≈ 25 samples / ~320 Hz) to its look-back history instead of
the phantom long-period autocorrelation peak that quiet input
otherwise produces. The silent frame still encodes through the
full voice pipeline; only the next frame’s look_back
continuity window is affected. Targets the 2-frame onset attack
at silence→loud transitions documented in
project_onset_attack_2frame_2026-04-25.md. Default off — eval
via the 5-vector PESQ harness before enabling in production.
Sourcepub fn default_pitch_on_silence(&self) -> bool
pub fn default_pitch_on_silence(&self) -> bool
Whether the onset-attack mitigation is currently enabled.
Sourcepub fn set_pyin_pitch(&mut self, on: bool)
pub fn set_pyin_pitch(&mut self, on: bool)
Enable or disable the PYIN pitch frontend. When on, the analysis
encoder uses crate::codecs::mbe_baseline::analysis::run_pyin
for (p_hat_i, e_p_hat_i) instead of the §0.3 look-back /
look-ahead tracker (Eq. 5–23). PYIN is post-2002 DSP outside
BABA-A’s clean-room scope; landed for A/B PESQ evaluation. The
§0.3 path remains the default and the spec-faithful baseline.
Sourcepub fn pyin_pitch(&self) -> bool
pub fn pyin_pitch(&self) -> bool
Whether the PYIN pitch frontend is currently enabled.
Sourcepub fn set_spectral_subtraction(&mut self, on: bool)
pub fn set_spectral_subtraction(&mut self, on: bool)
Enable or disable input-side spectral subtraction (Boll 1979)
applied to signal_spectrum output before §0.5 amplitude
estimation. Per-bin running noise PSD is updated on
silence-flagged frames; voiced frames hold. Off by default —
targets noisy-tone inputs (memo
project_amp_noise_sensitivity_2026-04-24).
Sourcepub fn spectral_subtraction(&self) -> bool
pub fn spectral_subtraction(&self) -> bool
Whether spectral subtraction is currently enabled.
Sourcepub fn set_amp_ema_alpha(&mut self, alpha: f64)
pub fn set_amp_ema_alpha(&mut self, alpha: f64)
Set the §0.5 amplitude EMA weight α. 0.0 disables the
smoother (default); (0.0, 1.0] enables
M̂_l(t) = α · M̂_l + (1−α) · M̂_l(t−1) per harmonic, gated by
pitch similarity. Targets the noisy-tone amp jitter described
in project_amp_noise_sensitivity_2026-04-24. Outside BABA-A
clean-room scope (general-DSP magnitude smoothing).
Sourcepub fn amp_ema_alpha(&self) -> f64
pub fn amp_ema_alpha(&self) -> f64
Current §0.5 amplitude EMA weight; 0.0 means the smoother is
off.
Sourcepub fn set_enhancement(&mut self, mode: EnhancementMode)
pub fn set_enhancement(&mut self, mode: EnhancementMode)
Configure the post-decoder enhancement chain. Off by default
(EnhancementMode::None — spec-faithful PCM). When set to
EnhancementMode::Classical, decoded PCM passes through a
biquad cascade + soft-knee compressor + boundary-fade chain
before Self::decode_bits returns. See
crate::enhancement for stage details and AIC33 mapping.
Resets the chain’s runtime filter state (delay lines, envelope,
pending fade) so the new mode starts clean. Persistent: stays
configured across Self::reset.
Sourcepub fn enhancement(&self) -> &EnhancementMode
pub fn enhancement(&self) -> &EnhancementMode
Currently configured enhancement mode.
Sourcepub fn builder(rate: Rate) -> VocoderBuilder
pub fn builder(rate: Rate) -> VocoderBuilder
Start a fluent builder for this rate. Equivalent to
VocoderBuilder::new(rate).
Sourcepub fn rate(&self) -> Rate
pub fn rate(&self) -> Rate
The rate this channel was constructed at. Cannot change for
the lifetime of the channel; build a new Vocoder to switch
rates (mirrors a chip’s PKT_RATEP cycle).
Sourcepub fn frame_samples(&self) -> usize
pub fn frame_samples(&self) -> usize
Number of i16 samples consumed per Self::encode_pcm call,
and produced per Self::decode_bits call.
Sourcepub fn fec_frame_bytes(&self) -> usize
pub fn fec_frame_bytes(&self) -> usize
Number of FEC bytes per encoded frame at this rate.
Sourcepub fn last_stats(&self) -> &FrameStats
pub fn last_stats(&self) -> &FrameStats
Read the most recent frame’s stats. Returns the zero-default before any frame has been processed.
Sourcepub fn last_disposition(&self) -> Option<FrameDisposition>
pub fn last_disposition(&self) -> Option<FrameDisposition>
Disposition (Use / Repeat / Mute) of the most recent
Self::decode_bits call’s synthesizer pass. None until at
least one frame has been decoded. Reset by Self::reset.
Decode-side only; encode-side has no synth and always returns
None.
Sourcepub fn reset(&mut self)
pub fn reset(&mut self)
Reset all channel state. Equivalent to the chip’s PKT_RATEP re-send: clears predictor history, decoder predictor state, synth substates, smoothed error rate, last-stats. Rate and configuration knobs (tone detection) stay the same.
Sourcepub fn encode_pcm(&mut self, pcm: &[i16]) -> Result<Vec<u8>, VocoderError>
pub fn encode_pcm(&mut self, pcm: &[i16]) -> Result<Vec<u8>, VocoderError>
Encode one PCM frame into FEC-encoded bytes.
pcm must be exactly Self::frame_samples samples (160).
Returns exactly Self::fec_frame_bytes bytes.
Sourcepub fn decode_bits(&mut self, bits: &[u8]) -> Result<Vec<i16>, VocoderError>
pub fn decode_bits(&mut self, bits: &[u8]) -> Result<Vec<i16>, VocoderError>
Decode one FEC-encoded frame into PCM samples.
bits must be exactly Self::fec_frame_bytes bytes.
Returns exactly Self::frame_samples PCM samples.
Sourcepub fn encode_stream<'a>(&'a mut self, pcm: &'a [i16]) -> EncodeStream<'a> ⓘ
pub fn encode_stream<'a>(&'a mut self, pcm: &'a [i16]) -> EncodeStream<'a> ⓘ
Encode an arbitrary-length PCM slice as a stream of frames.
Returns an iterator that yields one Result<Vec<u8>> per
frame consumed (160 samples per frame). Trailing partial
frames are silently dropped — the caller is responsible for
padding to a multiple of Self::frame_samples if all
samples must be encoded.
State (predictor, look-ahead history, ε_R) advances across
frames just as it would with manual per-frame
Self::encode_pcm calls.
let mut tx = Vocoder::new(Rate::Imbe7200x4400);
let bits: Result<Vec<Vec<u8>>, _> = tx.encode_stream(&pcm).collect();
assert_eq!(bits.unwrap().len(), 5);Sourcepub fn extract_params(&mut self, pcm: &[i16]) -> Result<MbeParams, VocoderError>
pub fn extract_params(&mut self, pcm: &[i16]) -> Result<MbeParams, VocoderError>
Run the analysis encoder on one PCM frame and return the
resulting MbeParams without quantizing or FEC-encoding.
Advances the analysis state (look-ahead history, predictor,
V/UV state, silence detector) so subsequent calls see
continuous context.
pcm must be exactly Self::frame_samples samples (160).
On AnalysisOutput::Silence (preroll, silence dispatch,
half-rate PitchOutOfRange), returns the rate-appropriate
silence params (MbeParams::silence or
MbeParams::silence_ambe_plus2).
Counterpart of Self::synthesize_params. Together these
expose the parameter layer without going through wire FEC,
enabling rate-conversion / analysis / playback pipelines that
don’t need the full encode/decode round-trip.
Sourcepub fn synthesize_params(&mut self, params: &MbeParams) -> Vec<i16>
pub fn synthesize_params(&mut self, params: &MbeParams) -> Vec<i16>
Synthesize one PCM frame directly from MbeParams, skipping
the FEC + dequantize chain. Advances the channel’s synth state
(so re-acquisition after silence and across-frame phase
continuity work the same way they would for a normal
Self::decode_bits call).
Useful for playing back tone-frame params from
crate::ambe_plus2_wire::dequantize::tone_to_mbe_params, replaying
captured params in test harnesses, or driving synth from any
upstream that produces MbeParams without going through wire
bits.
The dispatch is rate-aware: full-rate uses BABA-A baseline
phase; half-rate uses AMBE+2 (US5701390) phase regen, matching
what Self::decode_bits would do.
last_stats is not updated by this call — it’s reserved
for the wire-aware encode/decode paths. Read the synth’s
disposition via Self::last_disposition which IS advanced
(the disposition reflects this synth call).
Sourcepub fn decode_stream<'a>(&'a mut self, bits: &'a [u8]) -> DecodeStream<'a> ⓘ
pub fn decode_stream<'a>(&'a mut self, bits: &'a [u8]) -> DecodeStream<'a> ⓘ
Decode an arbitrary-length FEC byte slice as a stream of PCM frames.
Returns an iterator that yields one Result<Vec<i16>> per
frame consumed (Self::fec_frame_bytes per frame). Trailing
partial frames are silently dropped.
let mut rx = Vocoder::new(Rate::Imbe7200x4400);
let pcm_frames: Result<Vec<Vec<i16>>, _> = rx.decode_stream(&bits).collect();
assert_eq!(pcm_frames.unwrap().len(), 5);