The intelligent harmonizer — track a monophonic line, snap it to a key and scale, and add one or two harmony voices that stay IN KEY. More...
#include <pulp/music/pitch.hpp>#include <pulp/signal/dc_blocker.hpp>#include <pulp/signal/delay_line.hpp>#include <pulp/signal/denormal.hpp>#include <pulp/signal/pitch_shifter.hpp>#include <pulp/signal/rng.hpp>#include <pulp/signal/slew_limiter.hpp>#include <pulp/signal/units.hpp>#include <pulp/signal/yin_tracker.hpp>#include <algorithm>#include <cmath>#include <cstddef>#include <cstdint>#include <vector>Go to the source code of this file.
Classes | |
| struct | pulp::signal::DiatonicMapping |
| The result of one key+scale interval mapping. More... | |
| class | pulp::signal::DiatonicMapT< SampleType > |
| Key + scale interval mapper. More... | |
| class | pulp::signal::HarmonyEngineT< SampleType > |
| The diatonic harmonizer: YinTrackerT + DiatonicMapT + one PitchShifterT per voice. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
Typedefs | |
| using | pulp::signal::DiatonicMap = DiatonicMapT<float> |
| using | pulp::signal::DiatonicMap64 = DiatonicMapT<double> |
| using | pulp::signal::HarmonyEngine = HarmonyEngineT<float> |
| using | pulp::signal::HarmonyEngine64 = HarmonyEngineT<double> |
Enumerations | |
| enum class | pulp::signal::ScaleType : std::uint8_t { pulp::signal::major = 0 , pulp::signal::natural_minor = 1 , pulp::signal::dorian = 2 , pulp::signal::phrygian = 3 , pulp::signal::lydian = 4 , pulp::signal::mixolydian = 5 , pulp::signal::harmonic_minor = 6 , pulp::signal::melodic_minor = 7 , pulp::signal::major_pentatonic = 8 , pulp::signal::minor_pentatonic = 9 } |
| The shipped scale collection, order-locked. More... | |
| enum class | pulp::signal::OffScalePolicy : std::uint8_t { pulp::signal::nearest_lower , pulp::signal::nearest , pulp::signal::mute_wet } |
| How a pitch class that is not in the active scale is resolved. More... | |
Variables | |
| constexpr int | pulp::signal::kScaleCount = 10 |
| Number of entries in the shipped scale table. | |
| constexpr std::uint16_t | pulp::signal::kScaleTable [kScaleCount] |
| Pitch-class masks, bit k set ⇒ semitone k above the root is in the scale. | |
The intelligent harmonizer — track a monophonic line, snap it to a key and scale, and add one or two harmony voices that stay IN KEY.
RT contract: prepare(sample_rate) allocates the analysis and delay buffers once and may be called off the audio thread; set_*, process, and reset never allocate, never lock, and never throw. State is POD apart from the buffers prepare owns; a default-constructed instance is valid but must see prepare before process. — USE: reach for this when a single sung or played line should be doubled a musical interval away and STAY DIATONIC. A third above is 4 semitones on the tonic and 3 on the supertonic; that per-degree difference is the entire distance between a harmonizer and a pitch shifter with a knob. Reach for PitchShifterT directly when you want a fixed parallel transpose, an expression-pedal bend, or a dive-bomb — this block is the one that needs to know what key you are in.
The dual-tap crossfade method resamples the waveform, so it moves the entire spectral envelope with the pitch; preserving formants requires source-filter separation (LPC or cepstral envelope estimation) and independent envelope re-imposition, which is a phase-vocoder/LPC-tier technique outside this delay-domain block. There is no citable literature for formant-preserving crossfade-delay shifting because the method structurally cannot do it. Consequently this block exposes no formant preservation control; callers must choose a source-filter-capable tier.
Nothing here is nonlinear: the tracker is analysis-only, the mapper is integer arithmetic, and the voice sum is a static feed-forward mix. So there is no nonlinear aliasing to oversample away, and none is done. The resampling aliasing that up-shifting voices DO produce belongs to PitchShifterT and is documented there — it is inherited, not introduced.
There is no feedback path: the shifters read from a write-only ring and nothing routes output back into any buffer. The bound is therefore the closed-form arithmetic sum at the declared +6 dB parameter ceiling, including each pitch shifter's DC-blocker sample-gain bound. No invariant test is required because no loop exists — contrast the feedback designs in this series, where the registry number must cite a tested bound.
Published and cited at the constant: kYinThreshold = 0.10 (de Cheveigné & Kawahara 2002 §III.D), the YIN difference/CMND/threshold/parabolic steps themselves, and r = 2^(s/12) plus the diatonic step structure, which are equal temperament and public music theory — computed here, never tabulated as magic numbers. Everything else is a design parameter carrying a default and a range on its own constant.
HONEST GAPS: (a) formant preservation, above. (b) the octave-error median in YinTrackerT is an ORIGINAL heuristic and explicitly not YIN's step 6 — the paper's best-local-estimate step is omitted, and a cheaper 3-tap median across hops is substituted. Saying "we implement YIN" without that qualification would be a misattribution.
[1] A. de Cheveigné and H. Kawahara, "YIN, a fundamental frequency estimator for speech and music," JASA 111(4), pp. 1917–1930, April 2002 — the difference function, the cumulative mean normalized difference, the absolute threshold, and parabolic interpolation. Steps 1–5 implemented; step 6 deliberately not (see above). [2] J. Dattorro, "Effect Design, Part 2: Delay-Line Modulation and Chorus," JAES 45(10), pp. 764–788, October 1997 — the shifting topology, which lives in pitch_shifter.hpp rather than here.
"DigiTech-style" as used in the specification for this module names a DOCUMENTED PRODUCT-BEHAVIOUR CATEGORY — a diatonic harmonizer that follows a key and scale — and nothing else. No proprietary code, constants, or tables from any commercial harmonizer are used, referenced, or implied.