Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
scale_quantizer.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pulp/signal/detail/modular_sequencing_common.hpp>
4
5#include <algorithm>
6#include <cmath>
7#include <cstdint>
8
9namespace pulp::signal {
10
11// ── Quantizer ─────────────────────────────────────────────────────────────
12
14enum class QuantizeMode {
17};
18
52template <typename SampleType = float>
54public:
57 static constexpr int kDefaultEdo = 12;
58 static constexpr int kMaxEdo = 48;
59
61 static constexpr std::uint16_t kMajorMask = 0b0000'1010'1011'0101u;
62
66 static constexpr double kHystCents = 20.0;
67
72 static constexpr double kMaxHystCents = 1200.0;
73
87 static constexpr double kMaxHystSteps = 0.45;
88
98 static constexpr double kMaxAbsSteps = 4096.0;
99
100 void prepare(double sample_rate) {
101 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
102 }
103
104 void set_mode(QuantizeMode mode) { mode_ = mode; }
105 QuantizeMode mode() const { return mode_; }
106
107 void set_edo(int n) { edo_ = std::clamp(n, 1, kMaxEdo); }
108 int edo() const { return edo_; }
109
110 void set_scale_mask(std::uint16_t mask) { mask_ = static_cast<std::uint16_t>(mask & 0x0FFFu); }
111 std::uint16_t scale_mask() const { return mask_; }
112
113 void set_root_pc(int pc) { root_pc_ = ((pc % 12) + 12) % 12; }
114 int root_pc() const { return root_pc_; }
115
116 void set_hysteresis_cents(double cents) {
117 hyst_cents_ = seq_detail::clamp_finite(cents, 0.0, kMaxHystCents);
118 }
119 double hysteresis_cents() const { return hyst_cents_; }
120
123 void apply_reset_edge() { have_latch_ = false; }
124
125 void reset() {
126 have_latch_ = false;
127 latched_step_ = 0;
128 }
129
130 static constexpr int latency_samples() { return 0; }
131
134 int latched_step() const { return latched_step_; }
135
137 SampleType process(SampleType cv_volts) {
138 const int divisions = (mode_ == QuantizeMode::edo) ? edo_ : 12;
139 const double in_steps = static_cast<double>(cv_volts) * static_cast<double>(divisions);
140 const int step = latch(in_steps, divisions);
141 const int out_step = (mode_ == QuantizeMode::edo) ? step : snap_to_mask(step);
142 return static_cast<SampleType>(static_cast<double>(out_step) /
143 static_cast<double>(divisions));
144 }
145
146private:
148 static int round_half_up(double x) {
149 return static_cast<int>(
150 std::floor(seq_detail::clamp_finite(x, -kMaxAbsSteps, kMaxAbsSteps) + 0.5));
151 }
152
153 int latch(double in_steps, int divisions) {
154 const int candidate = round_half_up(in_steps);
155 if (!have_latch_) {
156 have_latch_ = true;
157 latched_step_ = candidate;
158 return latched_step_;
159 }
160
161 // One step spans 1200/divisions cents, so the window in steps is
162 // `cents · divisions / 1200`. At 12-TET and the 20-cent default this is
163 // 0.2 of a semitone, matching the spec's worked example. The cap only
164 // engages above EDO-30 — see `kMaxHystSteps` for why it has to.
165 const double window = std::min(hyst_cents_ * static_cast<double>(divisions) / 1200.0,
167 const double here = static_cast<double>(latched_step_);
168 if (candidate > latched_step_ && in_steps < here + 0.5 + window) return latched_step_;
169 if (candidate < latched_step_ && in_steps > here - 0.5 - window) return latched_step_;
170
171 latched_step_ = candidate;
172 return latched_step_;
173 }
174
175 bool allowed(int semitone) const {
176 int pc = (semitone - root_pc_) % 12;
177 if (pc < 0) pc += 12;
178 return ((mask_ >> pc) & 1u) != 0u;
179 }
180
181 int snap_to_mask(int semitone) const {
182 // An empty mask has no enabled pitch class to snap to. Passing the
183 // chromatic step through is the only answer that keeps the output in
184 // tune; silently substituting a scale would hide the misconfiguration.
185 if (mask_ == 0u) return semitone;
186 for (int d = 0; d <= 12; ++d) {
187 if (allowed(semitone + d)) return semitone + d; // ties resolve upward
188 if (d > 0 && allowed(semitone - d)) return semitone - d;
189 }
190 return semitone;
191 }
192
193 double sample_rate_ = 44100.0;
195 int edo_ = kDefaultEdo;
196 std::uint16_t mask_ = kMajorMask;
197 int root_pc_ = 0;
198 double hyst_cents_ = kHystCents;
199
200 bool have_latch_ = false;
201 int latched_step_ = 0;
202};
203
206
207} // namespace pulp::signal
CV-to-scale quantizer on the 1 V/octave standard.
Definition scale_quantizer.hpp:53
static constexpr int kDefaultEdo
Definition scale_quantizer.hpp:57
void apply_reset_edge()
Verb 2: clears the hysteresis latch, so the next input quantizes on its own merits rather than being ...
Definition scale_quantizer.hpp:123
int edo() const
Definition scale_quantizer.hpp:108
static constexpr int kMaxEdo
Definition scale_quantizer.hpp:58
static constexpr double kMaxHystCents
Definition scale_quantizer.hpp:72
void set_scale_mask(std::uint16_t mask)
Definition scale_quantizer.hpp:110
static constexpr double kMaxHystSteps
Definition scale_quantizer.hpp:87
std::uint16_t scale_mask() const
Definition scale_quantizer.hpp:111
void prepare(double sample_rate)
Definition scale_quantizer.hpp:100
void set_mode(QuantizeMode mode)
Definition scale_quantizer.hpp:104
static constexpr std::uint16_t kMajorMask
Definition scale_quantizer.hpp:61
int root_pc() const
Definition scale_quantizer.hpp:114
int latched_step() const
The latched pre-mask step, in units of 1/N octave (edo mode) or semitones (scale_mask mode).
Definition scale_quantizer.hpp:134
double hysteresis_cents() const
Definition scale_quantizer.hpp:119
QuantizeMode mode() const
Definition scale_quantizer.hpp:105
void reset()
Definition scale_quantizer.hpp:125
static constexpr double kHystCents
Definition scale_quantizer.hpp:66
void set_edo(int n)
Definition scale_quantizer.hpp:107
static constexpr int latency_samples()
Definition scale_quantizer.hpp:130
SampleType process(SampleType cv_volts)
Quantizes one sample of pitch CV, in volts.
Definition scale_quantizer.hpp:137
static constexpr double kMaxAbsSteps
Definition scale_quantizer.hpp:98
void set_root_pc(int pc)
Definition scale_quantizer.hpp:113
void set_hysteresis_cents(double cents)
Definition scale_quantizer.hpp:116
@ d
Definition pitch.hpp:22
Definition additive_bank.hpp:218
QuantizeMode
How a QuantizeScaleT divides the octave.
Definition scale_quantizer.hpp:14
@ edo
N equal steps per octave.
Definition scale_quantizer.hpp:15
@ scale_mask
12-TET, then snapped to a 12-bit pitch-class mask.
Definition scale_quantizer.hpp:16
QuantizeScaleT< double > QuantizeScale64
Definition scale_quantizer.hpp:205
@ u
Definition formant_filter_bank.hpp:20
QuantizeScaleT< float > QuantizeScale
Definition scale_quantizer.hpp:204