Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
harmony_engine.hpp File Reference

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.

Detailed Description

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.

Three stages, one block

  1. Track — YinTrackerT estimates the input fundamental (YIN).
  2. Map — DiatonicMapT quantizes it to the active key + scale and converts a requested interval IN SCALE STEPS into a concrete semitone shift.
  3. Shift — one PitchShifterT per voice does the actual shifting, driven from a cents-domain glide.

Composition — what this file does NOT own

  • The pitch shifter. The harmony voices ARE PitchShifterT instances (pitch_shifter.hpp) — the same Dattorro dual-tap crossfaded modulated delay the spec describes as CrossfadeShifterT, already built, already characterised. There is no second shifter here, and no crossfade, interpolation, or ring-buffer arithmetic in this file.
  • Glide — SlewLimiterT in SlewMode::linear, on the CENTS signal.
  • Units — units::hz_to_midi, midi_to_hz, semitones_to_ratio, ratio_to_cents, db_to_linear.
  • Ring storage — DelayLineT for the dry/alignment path.
  • Tracker front end — DcBlocker.
  • Humanise — DriftT (seeded Xorshift32 lineage), off by default.

What "intelligent" costs, stated up front

  • Monophonic only. YIN is a monophonic f0 estimator; polyphonic pitch tracking is a different and far harder problem. Feed this block one line. A chord produces a confident estimate of something, and the harmony will follow that something.
  • Latency is real and reported. A period-length autocorrelation cannot be zero-latency: it needs a window containing two periods of the lowest tracked pitch. At the shipped 80 Hz floor and 48 kHz that is 1200 samples = 25 ms. latency_samples() reports it, the dry path is delayed to match, and the shifters are pre-delayed so the wet legs land on the same instant (see latency_samples for why the dry delay alone is not enough).
  • The shifter's own character comes with it. These are ratio shifters: they warble, they move formants with pitch ("chipmunk" on upshift), and they alias on up-shift. All of that is documented on PitchShifterT and applies here unchanged.

Formant preservation — not exposed by this implementation tier

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.

Anti-aliasing policy (series law 4)

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.

Gain bound (series law 8)

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.

Constants: published vs design parameter

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.

References

[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.