The classic channel vocoder — Dudley's analysis-synthesis speech remaker. More...
#include <pulp/signal/ballistics_filter.hpp>#include <pulp/signal/dc_blocker.hpp>#include <pulp/signal/osc/va.hpp>#include <pulp/signal/rng.hpp>#include <pulp/signal/svf.hpp>#include <pulp/signal/tpt_filter.hpp>#include <pulp/signal/units.hpp>#include <pulp/signal/vca.hpp>#include <algorithm>#include <array>#include <cmath>#include <cstddef>#include <cstdint>Go to the source code of this file.
Classes | |
| class | pulp::signal::VocoderT< SampleType > |
| A classic channel vocoder: analysis bank → envelopes → matched synthesis bank. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
Typedefs | |
| using | pulp::signal::Vocoder = VocoderT<float> |
| using | pulp::signal::Vocoder64 = VocoderT<double> |
Enumerations | |
| enum class | pulp::signal::VocoderCarrier : std::uint8_t { pulp::signal::external , pulp::signal::internal } |
| Where the thing that speaks comes from. More... | |
| enum class | pulp::signal::VocoderWave : std::uint8_t { pulp::signal::saw , pulp::signal::pulse } |
| The internal carrier's shape. Both are polyBLEP-corrected. More... | |
The classic channel vocoder — Dudley's analysis-synthesis speech remaker.
A bank of bandpass filters measures a modulator (usually a voice) as a coarse, slowly-varying spectral envelope; a matched bank imposes that envelope on a spectrally rich carrier, one gain per band. The carrier speaks. That is the whole idea, and it is Homer Dudley's: reconstruct speech from a buzz source, a hiss source, and a set of band levels.
Three things separate a vocoder that works from one that does not, and all three are here rather than in a post-process:
Roland VP-330 (10-band vocoder in an ensemble bed), Moog 16-Channel Vocoder / Bode 7702 (16 bands, analysis→synthesis cross-matrix — the ancestor of set_formant_shift_semitones), Korg DVP-1 (a vocoder with its own internal carrier, which is why CarrierSource::internal exists). No filter coefficients, envelope constants or code from any of them are used.
The VP-330's signature is a vocoded voice sitting in a wide detuned bed, and the spec is explicit that this class owns no chorus state: the catalog node wires this module's mono output into a ChorusEnsembleT and crossfades by its own ensemble_amt. So there is no set_ensemble_amt() here and no include of chorus_family.hpp. The composition is not untested for being absent — the acceptance suite builds the node's wiring explicitly and asserts it, so the node author has an executable reference rather than a paragraph.
Published and cited at the constant: none of the numbers here are, and that is the honest position. What is published is the mechanism — the buzz/hiss excitation split and slowly-varying band levels (Dudley 1939), zero-crossing rate and high-frequency energy as voicing cues plus pre-emphasis (Flanagan 1972), quasi-logarithmic critical-band spacing as the reason to place centres geometrically (Zwicker 1961), and the TPT SVF topology (Zavalishin). Every numeric value below is a design parameter with a default and a range, or a derived identity (kCascadeBWFactor, Q_band) that is algebra on the shipped spacing rather than a citation. The classic band counts are documented behaviour, not sourced coefficients.
None required, and the reason is structural rather than fortunate. The signal path contains exactly two nonlinearities and neither reaches the output as broadband audio: the follower's rectifier feeds a one-pole lowpass, not the output, and the internal oscillator is band-limited by polyBLEP construction. The per-band multiply is a product of an audio signal with a control signal whose bandwidth is bounded by the follower — a ring-modulation whose sidebands sit within a few hundred Hz of the band they belong to, far from Nyquist. Nothing here would benefit from oversampling.
latency_samples() is 0. Every stage is minimum-phase IIR — TPT bandpass sections, one-pole followers, one-zero pre-emphasis, first-order high-passes. The banks impose frequency-dependent group delay, which is inherent to IIR filtering and is not a compensable bulk delay, so there is nothing to report and an impulse produces output on sample 0.
RT contract: prepare(sample_rate) computes coefficients and clears state; it allocates nothing, because every buffer is a fixed std::array sized at construction for kMaxBands and for the widest ZCR window at the highest supported rate. set_*, process and reset allocate nothing, take no locks and perform no I/O. set_band_count/set_band_range_hz recompute filter coefficients and follower floors — control-rate work, still allocation-free, and they change only the active loop bound, never the storage. Randomness is one Xorshift32 rewound by reset() to kNoiseSeed, and one sample is drawn per process() call regardless of carrier source, so a render is bit-identical per (params, input) and does not depend on which branch ran.