Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
harmony_engine.hpp
Go to the documentation of this file.
1#pragma once
2
4
119
124#include <pulp/signal/rng.hpp>
126#include <pulp/signal/units.hpp>
128
129#include <algorithm>
130#include <cmath>
131#include <cstddef>
132#include <cstdint>
133#include <vector>
134
135namespace pulp::signal {
136
153
155inline constexpr int kScaleCount = 10;
156
171
173enum class OffScalePolicy : std::uint8_t {
177};
178
183 int input_midi = 0;
184 int snapped_midi = 0;
185 int target_midi = 0;
186 int degree = 0;
187 bool chromatic = false;
188 bool clamped = false;
189};
190
191// ── Stage 2 — diatonic interval mapping ───────────────────────────────────
192
206template <typename SampleType = float>
208public:
211 static constexpr int kMaxDegrees = 12;
212
215 static constexpr int kIntervalStepsMax = 14;
216
222 static constexpr int kShiftSemitonesMax = 12;
223
224 DiatonicMapT() { rebuild(); }
225
227 void set_key(int root_pitch_class) {
228 root_ = ((root_pitch_class % 12) + 12) % 12;
229 }
230 int key() const { return root_; }
231
233 scale_ = scale;
234 rebuild();
235 }
236 ScaleType scale() const { return scale_; }
237
238 void set_off_scale_policy(OffScalePolicy policy) { policy_ = policy; }
239 OffScalePolicy off_scale_policy() const { return policy_; }
240
243 int degree_count() const { return degree_count_; }
244
247 int degree_semitone(int index) const {
248 if (index < 0 || index >= degree_count_) return 0;
249 return degrees_[static_cast<std::size_t>(index)];
250 }
251
254 DiatonicMapping map_midi(int midi_note, int interval_steps) const {
255 DiatonicMapping out;
256 out.input_midi = midi_note;
257 if (degree_count_ <= 0) return out;
258
259 const int steps = std::clamp(interval_steps, -kIntervalStepsMax, kIntervalStepsMax);
260
261 // Pitch class within the key, and the octave block it sits in.
262 const int rel = midi_note - root_;
263 const int pc = ((rel % 12) + 12) % 12;
264 const int octave = (rel - pc) / 12;
265
266 // Snap to a degree.
267 int degree = 0;
268 bool chromatic = true;
269 for (int i = 0; i < degree_count_; ++i) {
270 if (degrees_[static_cast<std::size_t>(i)] == pc) {
271 degree = i;
272 chromatic = false;
273 break;
274 }
275 }
276 if (chromatic) degree = snap_degree(pc);
277
278 out.chromatic = chromatic;
279 out.degree = degree;
280 out.snapped_midi =
281 root_ + 12 * octave + degrees_[static_cast<std::size_t>(degree)];
282
283 // Walk `steps` entries up (or down) the scale, wrapping the octave on
284 // the degree COUNT — which is what makes a "third" a scale relation
285 // rather than a fixed transposition.
286 const int target_degree = degree + steps;
287 const int wrapped = floor_div(target_degree, degree_count_);
288 const int index = target_degree - wrapped * degree_count_;
289 out.target_midi = root_ + 12 * (octave + wrapped) +
290 degrees_[static_cast<std::size_t>(index)];
291
292 int shift = out.target_midi - out.snapped_midi;
293 const int limited = std::clamp(shift, -kShiftSemitonesMax, kShiftSemitonesMax);
294 out.clamped = limited != shift;
295 out.shift_semitones = limited;
296 return out;
297 }
298
301 DiatonicMapping map_hz(double f0_hz, int interval_steps) const {
302 if (!(f0_hz > 0.0)) return DiatonicMapping{};
303 const int midi = static_cast<int>(
304 std::lround(units::hz_to_midi(f0_hz)));
305 return map_midi(midi, interval_steps);
306 }
307
308private:
311 static int floor_div(int a, int b) {
312 const int q = a / b;
313 return (a % b != 0 && ((a < 0) != (b < 0))) ? q - 1 : q;
314 }
315
321 int snap_degree(int pc) const {
322 int lower = degree_count_ - 1; // wraps to the degree below the octave
323 for (int i = degree_count_ - 1; i >= 0; --i) {
324 if (degrees_[static_cast<std::size_t>(i)] <= pc) {
325 lower = i;
326 break;
327 }
328 }
329 if (policy_ != OffScalePolicy::nearest) return lower;
330
331 const int upper = (lower + 1) % degree_count_;
332 const int upper_pc = degrees_[static_cast<std::size_t>(upper)] +
333 (upper == 0 ? 12 : 0);
334 const int down = pc - degrees_[static_cast<std::size_t>(lower)];
335 const int up = upper_pc - pc;
336 return up < down ? upper : lower; // ties → lower
337 }
338
339 void rebuild() {
340 const auto index = static_cast<std::size_t>(scale_);
341 const std::uint16_t mask =
342 index < static_cast<std::size_t>(kScaleCount) ? kScaleTable[index]
343 : kScaleTable[0];
344 degree_count_ = 0;
345 for (int k = 0; k < 12; ++k) {
346 if ((mask >> k) & 1u) {
347 degrees_[static_cast<std::size_t>(degree_count_)] = k;
348 ++degree_count_;
349 }
350 }
351 if (degree_count_ == 0) { // a mask with no root is not a scale
352 degrees_[0] = 0;
353 degree_count_ = 1;
354 }
355 }
356
357 int root_ = 0;
360 int degrees_[kMaxDegrees] = {};
361 int degree_count_ = 0;
362};
363
366
367// ── Stages 3 & 4 — voices, detune, glide ──────────────────────────────────
368
373template <typename SampleType = float>
375public:
380 static constexpr int kMaxVoices = 2;
381
386 static constexpr double kCrossfadeMsDefault = 20.0;
387 static constexpr double kCrossfadeMsMin = 10.0;
388 static constexpr double kCrossfadeMsMax = 50.0;
389
393 static constexpr double kGlideMsDefault = 60.0;
394 static constexpr double kGlideMsMax = 500.0;
395
399 static constexpr double kVoiceMuteMs = 10.0;
400
402 static constexpr double kDetuneMaxCents = 50.0;
403
406 static constexpr double kHumanizeDefault = 0.0;
407 static constexpr double kHumanizeMaxCents = 15.0;
408
411 static constexpr int kV1IntervalDefault = 2;
412 static constexpr int kV2IntervalDefault = 4;
413
416 static constexpr double kLevelMinDb = -60.0;
417 static constexpr double kLevelMaxDb = 6.0;
418 static constexpr double kLevelMaxLinear = 1.9952623149688795;
419
432 static constexpr double kWorstCaseGain =
435
439 static constexpr double kMuteFloor = 1e-5;
440
442 for (int v = 0; v < kMaxVoices; ++v) {
443 glide_[v].set_mode(SlewMode::linear);
444 glide_[v].set_time_ms(kGlideMsDefault);
445 interval_[v] = v == 0 ? kV1IntervalDefault : kV2IntervalDefault;
446 // Independent drift streams: one seed for both voices would make
447 // two "humanised" voices wander in lockstep, which is exactly the
448 // decorrelation the feature exists to provide.
449 drift_[v].set_seed(kDriftSeedBase ^
450 (0x9E3779B9u * static_cast<std::uint32_t>(v + 1)));
451 drift_[v].set_depth_percent(0.0);
452 }
453 mute_.set_mode(SlewMode::linear);
454 mute_.set_time_ms(kVoiceMuteMs);
455 enabled_[0] = true;
456 enabled_[1] = false;
457 }
458
461 void prepare(double sample_rate) {
462 sample_rate_ = (std::isfinite(sample_rate) && sample_rate > 0.0)
463 ? sample_rate
464 : 48000.0;
465 tracker_.prepare(sample_rate_);
466
467 for (int v = 0; v < kMaxVoices; ++v) {
468 auto& shifter = shifter_[static_cast<std::size_t>(v)];
469 shifter.prepare(sample_rate_);
470 shifter.set_shift_source(ShiftSource::direct);
471 shifter.set_window_ms(crossfade_ms_);
472 // The engine owns the glide, in cents, so the shifter's own
473 // semitone-domain slew must be out of the way — two serial glides
474 // would neither reach the target in `glide_ms` nor be linear in
475 // cents.
476 shifter.set_glide_ms(0.0, 0.0);
477 shifter.set_mix(1.0);
478 shifter.set_interp(interp_);
479 glide_[v].prepare(sample_rate_);
480 drift_[v].prepare(sample_rate_);
481 }
482 mute_.prepare(sample_rate_);
483
484 update_alignment();
485 // Sized for the WORST case, not the current one: `set_crossfade_ms` can
486 // grow the shifter's latency after `prepare`, and with a high `kF0Min`
487 // the shifter's window can exceed the tracker's. Sizing to the current
488 // latency would leave a later crossfade change reading past the end of
489 // the ring — which wraps rather than faults, so it would surface as a
490 // mysterious wrong-sounding dry path rather than as a crash.
491 const int worst_shifter_latency = static_cast<int>(
492 std::lround(kCrossfadeMsMax * sample_rate_ / 2000.0));
493 align_.prepare(std::max(tracker_.latency_samples(), worst_shifter_latency) + 1);
494 reset();
495 }
496
497 void reset() {
498 tracker_.reset();
499 align_.reset();
500 dc_.reset();
501 mute_.set_immediate(0.0);
502 for (int v = 0; v < kMaxVoices; ++v) {
503 shifter_[static_cast<std::size_t>(v)].reset();
504 drift_[v].reset();
505 shift_semitones_[v] = 0;
506 glide_[v].set_immediate(target_cents(v));
507 apply_ratio(v, glide_[v].value());
508 }
509 }
510
513 void discard_history() noexcept {
514 tracker_.discard_history();
515 align_.discard_history();
516 dc_.reset();
517 mute_.set_immediate(0.0);
518 for (int v = 0; v < kMaxVoices; ++v) {
519 shifter_[static_cast<std::size_t>(v)].discard_history();
520 drift_[v].reset();
521 shift_semitones_[v] = 0;
522 glide_[v].set_immediate(target_cents(v));
523 apply_ratio(v, glide_[v].value());
524 }
525 }
526
527 // ── Control surface ───────────────────────────────────────────────────
528
529 void set_key(int root_pitch_class) { map_.set_key(root_pitch_class); }
530 void set_scale(ScaleType scale) { map_.set_scale(scale); }
532 map_.set_off_scale_policy(policy);
533 }
534
537 void set_voice_interval(int voice, int steps) {
538 if (!valid(voice)) return;
539 interval_[voice] = std::clamp(steps, -DiatonicMapT<SampleType>::kIntervalStepsMax,
541 }
542 int voice_interval(int voice) const { return valid(voice) ? interval_[voice] : 0; }
543
544 void set_voice_detune_cents(int voice, double cents) {
545 if (!valid(voice) || !std::isfinite(cents)) return;
546 detune_cents_[voice] = clamp_finite(cents, -kDetuneMaxCents, kDetuneMaxCents);
547 }
548 double voice_detune_cents(int voice) const {
549 return valid(voice) ? detune_cents_[voice] : 0.0;
550 }
551
552 void set_voice_level_db(int voice, double db) {
553 if (!valid(voice) || !std::isfinite(db)) return;
554 level_db_[voice] = clamp_finite(db, kLevelMinDb, kLevelMaxDb);
555 level_[voice] = units::db_to_linear(level_db_[voice]);
556 }
557 double voice_level_db(int voice) const {
558 return valid(voice) ? level_db_[voice] : kLevelMinDb;
559 }
560
561 void set_voice_enabled(int voice, bool on) {
562 if (!valid(voice)) return;
563 enabled_[voice] = on;
564 }
565 bool voice_enabled(int voice) const { return valid(voice) && enabled_[voice]; }
566
567 void set_dry_level_db(double db) {
568 if (!std::isfinite(db)) return;
569 dry_db_ = clamp_finite(db, kLevelMinDb, kLevelMaxDb);
570 dry_ = units::db_to_linear(dry_db_);
571 }
572 double dry_level_db() const { return dry_db_; }
573
577 void set_glide_ms(double ms) {
578 if (!std::isfinite(ms)) return;
579 glide_ms_ = clamp_finite(ms, 0.0, kGlideMsMax);
580 for (auto& g : glide_) g.set_time_ms(glide_ms_);
581 }
582 double glide_ms() const { return glide_ms_; }
583
586 void set_humanize_cents(double cents) {
587 if (!std::isfinite(cents)) return;
588 humanize_cents_ = clamp_finite(cents, 0.0, kHumanizeMaxCents);
589 // DriftT's depth is a percentage of a multiplier near 1, and
590 // `cents = 1200·log2(m) ≈ (1200/ln2)·ε` for small ε — so the depth that
591 // produces a 1σ of `humanize_cents_` is this, not a free constant.
592 const double epsilon_sigma = humanize_cents_ / kCentsPerUnitEpsilon;
593 for (auto& d : drift_) d.set_depth_percent(100.0 * epsilon_sigma);
594 }
595 double humanize_cents() const { return humanize_cents_; }
596
598 void set_crossfade_ms(double ms) {
599 if (!std::isfinite(ms)) return;
600 crossfade_ms_ = clamp_finite(ms, kCrossfadeMsMin, kCrossfadeMsMax);
601 for (auto& s : shifter_) s.set_window_ms(crossfade_ms_);
602 update_alignment();
603 }
604 double crossfade_ms() const { return crossfade_ms_; }
605
606 void set_interp(PitchInterp interp) {
607 interp_ = interp;
608 for (auto& s : shifter_) s.set_interp(interp);
609 }
610
611 // ── Reporting ─────────────────────────────────────────────────────────
612
627 int latency_samples() const { return latency_; }
628
630 int tracker_latency_samples() const { return tracker_.latency_samples(); }
631
634 return shifter_[0].latency_samples();
635 }
636
637 double tracked_f0_hz() const { return tracker_.f0_hz(); }
638 bool voiced() const { return tracker_.voiced(); }
639
642 const DiatonicMapping& voice_mapping(int voice) const {
643 return mapping_[valid(voice) ? voice : 0];
644 }
645
647 double voice_cents(int voice) const {
648 return valid(voice) ? glide_[voice].value() : 0.0;
649 }
650 double voice_ratio(int voice) const {
651 return valid(voice) ? units::cents_to_ratio(glide_[voice].value()) : 1.0;
652 }
653 int voice_shift_semitones(int voice) const {
654 return valid(voice) ? shift_semitones_[voice] : 0;
655 }
656
658 double mute_gain() const { return mute_.value(); }
659
660 const YinTrackerT<SampleType>& tracker() const { return tracker_; }
661 const DiatonicMapT<SampleType>& diatonic_map() const { return map_; }
662
663 // ── Audio ─────────────────────────────────────────────────────────────
664
665 SampleType process(SampleType x) {
666 // An unprepared instance emits silence rather than faulting. The
667 // contract says `prepare` must precede `process`, but `DelayLineT::push`
668 // writes its buffer unguarded — so without this, a host that calls
669 // `process` early (which real hosts do) gets an out-of-bounds write
670 // rather than a diagnosable silence.
671 if (latency_ <= 0) return SampleType{0};
672
673 if (!std::isfinite(static_cast<double>(x))) {
675 return SampleType{0};
676 }
677
678 const double in = static_cast<double>(x);
679
680 // Stage 1. The tracker sees a DC-blocked copy: a DC offset biases the
681 // difference function toward long lags and costs low-note accuracy.
682 if (tracker_.process(dc_.process(static_cast<SampleType>(in)))) {
683 // A new estimate landed — re-run the mapping. Between hops the
684 // mapper output is held and only the glide keeps moving.
685 update_targets();
686 }
687
688 // The alignment line carries the input for BOTH legs: the dry is read
689 // at the full reported latency, the shifters at the pre-delay so their
690 // own throughput lands them on the same instant.
691 align_.push(static_cast<SampleType>(in));
692 const double dry = static_cast<double>(align_.read(latency_));
693 const double shifter_in = static_cast<double>(align_.read(pre_delay_));
694
695 const double gate = mute_.process(tracker_.voiced() ? 1.0 : 0.0);
696
697 double wet_sum = 0.0;
698 for (int v = 0; v < kMaxVoices; ++v) {
699 if (!enabled_[v]) continue;
700
701 // Stage 3 control cadence: the cents ramp advances every sample so
702 // the glide is smooth between hops, and the ratio follows it.
703 double cents = glide_[v].process(target_cents(v));
704 if (humanize_cents_ > 0.0)
705 cents += units::ratio_to_cents(static_cast<double>(drift_[v].next()));
706 apply_ratio(v, cents);
707
708 const double wet = static_cast<double>(
709 shifter_[static_cast<std::size_t>(v)].process_wet(
710 static_cast<SampleType>(shifter_in)));
711 if (gate > kMuteFloor) wet_sum += level_[v] * gate * wet;
712 }
713
714 return static_cast<SampleType>(dry_ * dry + wet_sum);
715 }
716
717private:
720 static constexpr double kCentsPerUnitEpsilon = 1731.2340490667560888;
721
723 static constexpr std::uint32_t kDriftSeedBase = 0x48524D4Eu; // 'HRMN'
724
725 static bool valid(int voice) { return voice >= 0 && voice < kMaxVoices; }
726
727 static double clamp_finite(double v, double lo, double hi) {
728 if (!std::isfinite(v)) return lo;
729 return std::clamp(v, lo, hi);
730 }
731
734 double target_cents(int voice) const {
735 return 100.0 * static_cast<double>(shift_semitones_[voice]) +
736 detune_cents_[voice];
737 }
738
739 void apply_ratio(int voice, double cents) {
740 shifter_[static_cast<std::size_t>(voice)].set_shift_semitones(cents / 100.0);
741 }
742
744 void update_targets() {
745 if (!tracker_.voiced()) return; // hold through unvoiced frames
746 for (int v = 0; v < kMaxVoices; ++v) {
747 mapping_[v] = map_.map_hz(tracker_.f0_hz(), interval_[v]);
748 shift_semitones_[v] = mapping_[v].shift_semitones;
749 }
750 }
751
752 void update_alignment() {
753 const int tracker_latency = tracker_.latency_samples();
754 const int shifter_latency = shifter_[0].latency_samples();
755 latency_ = std::max(tracker_latency, shifter_latency);
756 pre_delay_ = latency_ - shifter_latency;
757 }
758
759 // ── Composed primitives ───────────────────────────────────────────────
760 YinTrackerT<SampleType> tracker_{};
761 DiatonicMapT<SampleType> map_{};
762 PitchShifterT<SampleType> shifter_[kMaxVoices]{};
763 DelayLineT<SampleType> align_{};
764 DcBlocker<SampleType> dc_{};
765 ConstantTimeSlewLimiterT<double> glide_[kMaxVoices]{};
766 ConstantTimeSlewLimiterT<double> mute_{};
767 DriftT<double> drift_[kMaxVoices]{};
768
769 // ── Configuration ─────────────────────────────────────────────────────
770 double sample_rate_ = 48000.0;
771 double crossfade_ms_ = kCrossfadeMsDefault;
772 double glide_ms_ = kGlideMsDefault;
773 double humanize_cents_ = kHumanizeDefault;
774 double dry_db_ = 0.0;
775 double dry_ = 1.0;
778 double detune_cents_[kMaxVoices] = {0.0, 0.0};
779 double level_db_[kMaxVoices] = {0.0, 0.0};
780 double level_[kMaxVoices] = {1.0, 1.0};
781 bool enabled_[kMaxVoices] = {true, false};
782
783 // ── Running state ─────────────────────────────────────────────────────
784 DiatonicMapping mapping_[kMaxVoices] = {};
785 int shift_semitones_[kMaxVoices] = {0, 0};
786 int latency_ = 0;
787 int pre_delay_ = 0;
788};
789
792
793} // namespace pulp::signal
Key + scale interval mapper.
Definition harmony_engine.hpp:207
void set_key(int root_pitch_class)
Root pitch class, 0 = C .. 11 = B.
Definition harmony_engine.hpp:227
void set_off_scale_policy(OffScalePolicy policy)
Definition harmony_engine.hpp:238
OffScalePolicy off_scale_policy() const
Definition harmony_engine.hpp:239
int key() const
Definition harmony_engine.hpp:230
DiatonicMapping map_midi(int midi_note, int interval_steps) const
Maps a chromatic MIDI note.
Definition harmony_engine.hpp:254
static constexpr int kShiftSemitonesMax
Definition harmony_engine.hpp:222
static constexpr int kMaxDegrees
Definition harmony_engine.hpp:211
static constexpr int kIntervalStepsMax
Definition harmony_engine.hpp:215
int degree_semitone(int index) const
Semitone offset of degree index above the root.
Definition harmony_engine.hpp:247
void set_scale(ScaleType scale)
Definition harmony_engine.hpp:232
DiatonicMapping map_hz(double f0_hz, int interval_steps) const
Maps a tracked frequency: rounds to the nearest chromatic note, then map_midi.
Definition harmony_engine.hpp:301
int degree_count() const
Number of degrees in the active scale — 7 for the modes, 5 for the pentatonics.
Definition harmony_engine.hpp:243
ScaleType scale() const
Definition harmony_engine.hpp:236
DiatonicMapT()
Definition harmony_engine.hpp:224
The diatonic harmonizer: YinTrackerT + DiatonicMapT + one PitchShifterT per voice.
Definition harmony_engine.hpp:374
void set_voice_detune_cents(int voice, double cents)
Definition harmony_engine.hpp:544
static constexpr double kLevelMinDb
Definition harmony_engine.hpp:416
static constexpr double kGlideMsMax
Definition harmony_engine.hpp:394
void set_humanize_cents(double cents)
Seeded humanise depth in cents, 1σ.
Definition harmony_engine.hpp:586
static constexpr int kV2IntervalDefault
Definition harmony_engine.hpp:412
static constexpr double kLevelMaxLinear
Definition harmony_engine.hpp:418
int shifter_latency_samples() const
The shifter's own throughput delay, on its own.
Definition harmony_engine.hpp:633
static constexpr double kWorstCaseGain
Definition harmony_engine.hpp:432
double crossfade_ms() const
Definition harmony_engine.hpp:604
void set_key(int root_pitch_class)
Definition harmony_engine.hpp:529
bool voiced() const
Definition harmony_engine.hpp:638
static constexpr double kCrossfadeMsMax
Definition harmony_engine.hpp:388
const DiatonicMapT< SampleType > & diatonic_map() const
Definition harmony_engine.hpp:661
void prepare(double sample_rate)
Sizes the tracker, the alignment delay, and both shifters.
Definition harmony_engine.hpp:461
void set_glide_ms(double ms)
Cents-domain portamento time.
Definition harmony_engine.hpp:577
const DiatonicMapping & voice_mapping(int voice) const
The mapping the engine last computed for a voice, including whether the input was off-scale or the sh...
Definition harmony_engine.hpp:642
double voice_level_db(int voice) const
Definition harmony_engine.hpp:557
static constexpr int kMaxVoices
Definition harmony_engine.hpp:380
double humanize_cents() const
Definition harmony_engine.hpp:595
int voice_shift_semitones(int voice) const
Definition harmony_engine.hpp:653
void set_voice_enabled(int voice, bool on)
Definition harmony_engine.hpp:561
bool voice_enabled(int voice) const
Definition harmony_engine.hpp:565
double voice_ratio(int voice) const
Definition harmony_engine.hpp:650
void set_dry_level_db(double db)
Definition harmony_engine.hpp:567
int voice_interval(int voice) const
Definition harmony_engine.hpp:542
double mute_gain() const
The wet-mute gate, 0 while unvoiced and 1 while voiced.
Definition harmony_engine.hpp:658
int tracker_latency_samples() const
The tracker window W, on its own.
Definition harmony_engine.hpp:630
int latency_samples() const
The delay the whole block imposes, and the delay the dry path is given.
Definition harmony_engine.hpp:627
double dry_level_db() const
Definition harmony_engine.hpp:572
static constexpr double kHumanizeMaxCents
Definition harmony_engine.hpp:407
static constexpr double kGlideMsDefault
Definition harmony_engine.hpp:393
static constexpr double kHumanizeDefault
Definition harmony_engine.hpp:406
const YinTrackerT< SampleType > & tracker() const
Definition harmony_engine.hpp:660
void set_scale(ScaleType scale)
Definition harmony_engine.hpp:530
void set_off_scale_policy(OffScalePolicy policy)
Definition harmony_engine.hpp:531
HarmonyEngineT()
Definition harmony_engine.hpp:441
SampleType process(SampleType x)
Definition harmony_engine.hpp:665
double glide_ms() const
Definition harmony_engine.hpp:582
static constexpr double kMuteFloor
Definition harmony_engine.hpp:439
static constexpr double kLevelMaxDb
Definition harmony_engine.hpp:417
double tracked_f0_hz() const
Definition harmony_engine.hpp:637
void discard_history() noexcept
Constant-time audio fault recovery.
Definition harmony_engine.hpp:513
void set_voice_interval(int voice, int steps)
Voice interval in SCALE STEPS, not semitones.
Definition harmony_engine.hpp:537
static constexpr double kVoiceMuteMs
Definition harmony_engine.hpp:399
static constexpr double kDetuneMaxCents
Definition harmony_engine.hpp:402
static constexpr double kCrossfadeMsDefault
Definition harmony_engine.hpp:386
void set_interp(PitchInterp interp)
Definition harmony_engine.hpp:606
void set_voice_level_db(int voice, double db)
Definition harmony_engine.hpp:552
double voice_detune_cents(int voice) const
Definition harmony_engine.hpp:548
void set_crossfade_ms(double ms)
The shifter's crossfade window, shared by both voices.
Definition harmony_engine.hpp:598
double voice_cents(int voice) const
The voice's post-glide offset in cents, and the ratio derived from it.
Definition harmony_engine.hpp:647
static constexpr int kV1IntervalDefault
Definition harmony_engine.hpp:411
static constexpr double kCrossfadeMsMin
Definition harmony_engine.hpp:387
void reset()
Definition harmony_engine.hpp:497
static constexpr double kDcBlockerPeakGain
The DC blocker's TIME-DOMAIN peak gain: the L1 norm of its impulse response, which is exactly 2 for e...
Definition pitch_shifter.hpp:353
Monophonic fundamental-frequency estimator, after de Cheveigné & Kawahara 2002 [1],...
Definition yin_tracker.hpp:30
Definition custom_node_events.hpp:26
constexpr std::array< ScaleCompatibilityEntry, 10 > kPulpSignalScales
Definition pitch.hpp:184
constexpr std::optional< PitchClassSet > scale_intervals(NamedScale scale) noexcept
Definition pitch.hpp:243
@ index
Definition gpu_pass.hpp:140
T cents_to_ratio(T cents)
Cents → frequency ratio. +100 → 2^(1/12), +1200 → 2.
Definition units.hpp:118
T ratio_to_cents(T ratio)
Frequency ratio → cents. Inverse of cents_to_ratio.
Definition units.hpp:125
T db_to_linear(T db)
Amplitude dB → linear gain. 0 dB → 1, −6.0206 dB → 0.5.
Definition units.hpp:71
T hz_to_midi(T hz)
Hz → MIDI note number.
Definition units.hpp:97
Definition additive_bank.hpp:218
constexpr int kScaleCount
Number of entries in the shipped scale table.
Definition harmony_engine.hpp:155
DiatonicMapT< float > DiatonicMap
Definition harmony_engine.hpp:364
HarmonyEngineT< double > HarmonyEngine64
Definition harmony_engine.hpp:791
@ linear
Definition mod_tools.hpp:106
constexpr std::uint16_t kScaleTable[kScaleCount]
Pitch-class masks, bit k set ⇒ semitone k above the root is in the scale.
Definition harmony_engine.hpp:159
ScaleType
The shipped scale collection, order-locked.
Definition harmony_engine.hpp:141
@ lydian
{0,2,4,6,7,9,11}
Definition harmony_engine.hpp:146
@ minor_pentatonic
{0,3,5,7,10}
Definition harmony_engine.hpp:151
@ natural_minor
Aeolian {0,2,3,5,7,8,10}.
Definition harmony_engine.hpp:143
@ phrygian
{0,1,3,5,7,8,10}
Definition harmony_engine.hpp:145
@ melodic_minor
ascending {0,2,3,5,7,9,11}
Definition harmony_engine.hpp:149
@ dorian
{0,2,3,5,7,9,10}
Definition harmony_engine.hpp:144
@ mixolydian
{0,2,4,5,7,9,10}
Definition harmony_engine.hpp:147
@ harmonic_minor
{0,2,3,5,7,8,11}
Definition harmony_engine.hpp:148
@ major_pentatonic
{0,2,4,7,9}
Definition harmony_engine.hpp:150
@ major
Ionian {0,2,4,5,7,9,11}.
Definition harmony_engine.hpp:142
@ up
Upper sideband on both channels: f → f + Δf.
Definition frequency_shifter_ssb.hpp:315
@ down
Lower sideband on both channels: f → f − Δf.
Definition frequency_shifter_ssb.hpp:316
DiatonicMapT< double > DiatonicMap64
Definition harmony_engine.hpp:365
PitchInterp
Fractional-delay read quality for the two taps.
Definition pitch_shifter.hpp:180
@ linear
2-point. Dattorro's recommendation for modulated delays.
Definition pitch_shifter.hpp:181
HarmonyEngineT< float > HarmonyEngine
Definition harmony_engine.hpp:790
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
OffScalePolicy
How a pitch class that is not in the active scale is resolved.
Definition harmony_engine.hpp:173
@ nearest_lower
Snap DOWN to the nearest in-scale degree (ties → lower).
Definition harmony_engine.hpp:174
@ nearest
Snap to the nearest in-scale degree in either direction.
Definition harmony_engine.hpp:175
@ mute_wet
Report it and let the caller duck the wet voice.
Definition harmony_engine.hpp:176
@ direct
set_shift_semitones. Detents and the pedal law are bypassed.
Definition pitch_shifter.hpp:194
@ k
Definition input_events.hpp:40
@ v
Definition input_events.hpp:41
The time-domain ratio pitch shifter — the one that warbles.
Deterministic randomness for modulation: a counter-free PRNG, a Gaussian draw, a stateless purpose-ke...
Compatibility include for the canonical modulation-toolkit slew limiter and sample-and-hold implement...
The result of one key+scale interval mapping.
Definition harmony_engine.hpp:181
bool chromatic
The input was off-scale and had to be snapped.
Definition harmony_engine.hpp:187
int shift_semitones
The concrete shift. THIS is the "intelligent" bit.
Definition harmony_engine.hpp:182
int snapped_midi
The in-scale note the input was snapped to.
Definition harmony_engine.hpp:184
bool clamped
The shift hit the ±1 octave ratio clamp.
Definition harmony_engine.hpp:188
int target_midi
The in-scale note the harmony voice plays.
Definition harmony_engine.hpp:185
int degree
Index of snapped_midi in the scale's degree list.
Definition harmony_engine.hpp:186
int input_midi
The chromatic note the tracker was rounded to.
Definition harmony_engine.hpp:183
The real-unit vocabulary shared by every DSP block in the catalog.
Standalone monophonic YIN pitch tracker used by the harmony engine.