92 engine_.set_sample_rate(sample_rate);
101 engine_.set_oversampling(factor);
109 if (voicing_ ==
voicing)
return;
119 void set_cutoff(SampleType knob01, SampleType modulation_octaves = 0) noexcept {
120 const double cutoff = std::clamp(
static_cast<double>(knob01), 0.0, 1.0);
121 const double modulation =
122 std::clamp(
static_cast<double>(modulation_octaves), -16.0, 16.0);
123 if (cutoff_knob_ == cutoff && cutoff_modulation_octaves_ == modulation)
125 cutoff_knob_ = cutoff;
126 cutoff_modulation_octaves_ = modulation;
133 const double resonance = std::clamp(
static_cast<double>(knob01), 0.0, 1.0);
134 if (resonance_knob_ == resonance)
return;
135 resonance_knob_ = resonance;
142 const double drive =
static_cast<double>(drive_db);
143 if (drive_db_ == drive)
return;
145 engine_.set_drive_db(drive_db_);
158 SampleType resonance_knob01, SampleType drive_db)
noexcept {
159 const double cutoff =
160 std::clamp(
static_cast<double>(cutoff_knob01), 0.0, 1.0);
161 const double modulation =
162 std::clamp(
static_cast<double>(modulation_octaves), -16.0, 16.0);
163 const double resonance =
164 std::clamp(
static_cast<double>(resonance_knob01), 0.0, 1.0);
165 const double drive =
static_cast<double>(drive_db);
166 const bool calibration_changed =
167 cutoff_knob_ != cutoff ||
168 cutoff_modulation_octaves_ != modulation ||
169 resonance_knob_ != resonance;
170 const bool drive_changed = drive_db_ != drive;
171 if (!calibration_changed && !drive_changed)
return;
173 cutoff_knob_ = cutoff;
174 cutoff_modulation_octaves_ = modulation;
175 resonance_knob_ = resonance;
180 if (calibration_changed)
183 engine_.set_drive_db(drive_db_);
190 engine_.set_smoothing_time_ms(milliseconds);
195 SampleType
process(SampleType input)
noexcept {
196 return engine_.process(input);
202 engine_.process(
buffer, num_samples);
214 return engine_.latency_samples();
241 double modulation_octaves = 0.0) noexcept {
242 const double cutoff = std::clamp(knob01, 0.0, 1.0);
243 const double modulation = std::clamp(modulation_octaves, -16.0, 16.0);
244 return log_frequency_table(cutoff, calibration_tables(
voicing).cutoff) *
245 std::exp2(modulation);
250 return engine_.oversampling();
260 std::span<const double> knots;
261 std::span<const double> values;
264 struct CalibrationTables {
269 static double linear_table(
double knob, TableView table)
noexcept {
270 const auto knots = table.knots;
271 const auto values = table.values;
272 if (knob <= knots.front())
273 return values.front();
274 if (knob >= knots.back())
275 return values.back();
276 for (std::size_t
i = 1;
i < knots.size(); ++
i) {
277 if (knob <= knots[
i]) {
278 const double fraction = (knob - knots[
i - 1]) / (knots[
i] - knots[
i - 1]);
279 return values[
i - 1] + fraction * (values[
i] - values[
i - 1]);
282 return values.back();
285 static double log_frequency_table(
double knob, TableView table)
noexcept {
286 const auto knots = table.knots;
287 const auto values = table.values;
288 if (knob <= knots.front())
289 return values.front();
290 if (knob >= knots.back())
291 return values.back();
292 for (std::size_t
i = 1;
i < knots.size(); ++
i) {
293 if (knob <= knots[
i]) {
294 const double fraction = (
knob - knots[
i - 1]) / (knots[
i] - knots[
i - 1]);
295 return values[
i - 1] * std::pow(values[
i] / values[
i - 1], fraction);
298 return values.back();
301 static double invert_log_frequency_table(
double frequency, TableView table)
noexcept {
302 const auto knots = table.knots;
303 const auto values = table.values;
304 if (frequency <= values.front())
305 return knots.front();
306 if (frequency >= values.back())
308 for (std::size_t
i = 1;
i < knots.size(); ++
i) {
309 if (frequency <= values[
i]) {
310 const double fraction =
311 std::log(frequency / values[
i - 1]) / std::log(values[
i] / values[
i - 1]);
312 return knots[
i - 1] + fraction * (knots[
i] - knots[
i - 1]);
318 static CalibrationTables calibration_tables(
Voicing voicing)
noexcept {
321 return {{kJunoCutoffKnots, kJunoCutoffHz}, {kJunoResonanceKnots, kJunoResonanceValues}};
323 return {{kJupiterCutoffKnots, kJupiterCutoffHz},
324 {kJupiterResonanceKnots, kJupiterResonanceValues}};
326 return {{kProphetCutoffKnots, kProphetCutoffHz},
327 {kProphetResonanceKnots, kProphetResonanceValues}};
329 return {{kMinimoogCutoffKnots, kMinimoogCutoffHz},
330 {kMinimoogResonanceKnots, kMinimoogResonanceValues}};
332 return {{kJunoCutoffKnots, kJunoCutoffHz}, {kJunoResonanceKnots, kJunoResonanceValues}};
335 void update_voicing() noexcept {
336 const CalibrationTables tables = calibration_tables(voicing_);
338 voicing_, cutoff_knob_, cutoff_modulation_octaves_);
339 const double equivalent_knob =
340 invert_log_frequency_table(corner_hz_, tables.cutoff);
341 const double resonance =
342 linear_table(resonance_knob_, tables.resonance);
345 engine_.set_bias(0.0);
346 engine_.set_drive_db(drive_db_);
350 engine_.set_k_max(4.30);
351 engine_.set_resonance(resonance);
352 engine_.set_compensation(0.21);
353 engine_.set_saturation_headroom(1.0);
354 engine_.set_output_gain(1.0);
355 engine_.set_cross_modulation(0.0, 2.0);
356 engine_.set_drift(0.0, 0.0);
357 engine_.set_pole_frequency(corner_hz_ / 0.4346);
361 engine_.set_k_max(3.53);
362 engine_.set_resonance(resonance);
363 engine_.set_compensation(0.14);
364 engine_.set_saturation_headroom(1.5);
365 engine_.set_output_gain(1.0);
366 engine_.set_cross_modulation(0.0, 2.0);
367 engine_.set_drift(0.0, 0.0);
368 engine_.set_pole_frequency(corner_hz_ / 0.4346);
372 const double compensation =
373 linear_table(resonance_knob_, {kProphetLawKnots, kProphetCompensation});
374 const double headroom =
375 linear_table(resonance_knob_, {kProphetLawKnots, kProphetHeadroom});
376 const double cross_mod =
377 linear_table(resonance_knob_, {kProphetLawKnots, kProphetCrossMod});
378 engine_.set_k_max(4.30);
379 engine_.set_resonance(resonance);
380 engine_.set_compensation(compensation);
381 engine_.set_saturation_headroom(headroom);
382 engine_.set_output_gain(1.0);
383 engine_.set_cross_modulation(cross_mod, 2.0);
384 engine_.set_drift(0.0, 0.0);
385 engine_.set_pole_frequency(corner_hz_ / 0.4346);
390 constexpr double kFloorMakeup = 1.3396;
391 const double makeup =
392 linear_table(equivalent_knob, {kMinimoogMakeupKnots, kMinimoogMakeup});
393 const double cutoff_headroom =
394 linear_table(equivalent_knob, {kMinimoogHeadroomKnots, kMinimoogHeadroom});
395 const double drop_db =
396 linear_table(resonance_knob_, {kMinimoogLawKnots, kMinimoogDropDb});
397 const double headroom_multiplier =
398 linear_table(resonance_knob_, {kMinimoogLawKnots, kMinimoogHeadroomMultiplier});
399 const double cross_mod =
400 linear_table(resonance_knob_, {kMinimoogLawKnots, kMinimoogCrossMod});
401 const double cutoff_cents =
402 linear_table(resonance_knob_, {kMinimoogLawKnots, kMinimoogCutoffDriftCents});
403 const double resonance_fraction =
404 linear_table(resonance_knob_, {kMinimoogLawKnots, kMinimoogResonanceDriftFraction});
405 const double drift_fade =
408 : (corner_hz_ >= 3000.0 ? 0.0 : (3000.0 - corner_hz_) / (3000.0 - 900.0));
410 engine_.set_k_max(4.30);
411 engine_.set_resonance(resonance);
412 engine_.set_compensation(0.0);
413 engine_.set_saturation_headroom(cutoff_headroom * headroom_multiplier / kFloorMakeup);
414 engine_.set_output_gain(makeup * std::pow(10.0, drop_db / 20.0));
415 engine_.set_cross_modulation(cross_mod, 2.0);
416 engine_.set_drift(cutoff_cents * drift_fade, resonance_fraction * drift_fade, 1.5,
418 engine_.set_pole_frequency(corner_hz_ / 0.678);
424 inline static constexpr std::array<double, 13> kJunoCutoffKnots{
425 0.0, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 1.0};
426 inline static constexpr std::array<double, 13> kJunoCutoffHz{
427 20.0, 45.0, 54.0, 72.0, 107.0, 166.0, 262.0, 424.0, 692.0, 1135.0, 1895.0, 3234.0, 18000.0};
428 inline static constexpr std::array<double, 12> kJunoResonanceKnots{
429 0.0, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1.0};
430 inline static constexpr std::array<double, 12> kJunoResonanceValues{
431 0.0, 0.213, 0.314, 0.407, 0.490, 0.573, 0.653, 0.743, 0.775, 0.870, 0.93, 1.0};
433 inline static constexpr std::array<double, 8> kJupiterCutoffKnots{0.0, 0.25, 0.35, 0.45,
434 0.55, 0.65, 0.75, 1.0};
435 inline static constexpr std::array<double, 8> kJupiterCutoffHz{20.0, 87.0, 157.0, 342.0,
436 760.0, 1668.0, 3440.0, 18000.0};
437 inline static constexpr std::array<double, 7> kJupiterResonanceKnots{0.0, 0.25, 0.50, 0.60,
439 inline static constexpr std::array<double, 7> kJupiterResonanceValues{
440 0.0, 0.349, 0.579, 0.622, 0.793, 0.921, 1.0};
442 inline static constexpr std::array<double, 10> kProphetCutoffKnots{0.0, 0.25, 0.35, 0.45, 0.55,
443 0.65, 0.75, 0.85, 0.95, 1.0};
444 inline static constexpr std::array<double, 10> kProphetCutoffHz{
445 4.0, 17.8, 35.6, 71.3, 142.6, 287.8, 573.0, 1151.2, 2291.0, 3172.0};
446 inline static constexpr std::array<double, 7> kProphetResonanceKnots{0.0, 0.25, 0.50, 0.60,
448 inline static constexpr std::array<double, 7> kProphetResonanceValues{
449 0.0, 0.583, 0.7634, 0.963, 0.975, 0.99, 1.0};
450 inline static constexpr std::array<double, 4> kProphetLawKnots{0.0, 0.60, 0.90, 1.0};
451 inline static constexpr std::array<double, 4> kProphetCompensation{0.0, 0.0, 0.066, 0.083};
452 inline static constexpr std::array<double, 4> kProphetHeadroom{1.0, 1.0, 1.90, 2.21};
453 inline static constexpr std::array<double, 4> kProphetCrossMod{0.0, 0.0, 0.30, 0.33};
455 inline static constexpr std::array<double, 9> kMinimoogCutoffKnots{0.0, 0.35, 0.45, 0.55, 0.65,
456 0.75, 0.85, 0.95, 1.0};
457 inline static constexpr std::array<double, 9> kMinimoogCutoffHz{
458 20.0, 126.0, 347.0, 821.0, 1641.0, 3414.0, 7393.0, 12975.0, 17187.0};
459 inline static constexpr std::array<double, 7> kMinimoogResonanceKnots{0.0, 0.25, 0.50, 0.60,
461 inline static constexpr std::array<double, 7> kMinimoogResonanceValues{
462 0.079, 0.079, 0.079, 0.52, 0.90, 0.97, 1.0};
463 inline static constexpr std::array<double, 4> kMinimoogMakeupKnots{0.0, 0.10, 0.30, 1.0};
464 inline static constexpr std::array<double, 4> kMinimoogMakeup{1.0, 1.0, 1.3396, 1.3396};
465 inline static constexpr std::array<double, 7> kMinimoogHeadroomKnots{0.35, 0.55, 0.65, 0.75,
467 inline static constexpr std::array<double, 7> kMinimoogHeadroom{1.0, 0.72, 1.163, 1.70,
469 inline static constexpr std::array<double, 4> kMinimoogLawKnots{0.0, 0.60, 0.90, 1.0};
470 inline static constexpr std::array<double, 4> kMinimoogDropDb{0.0, 0.0, -2.3, -3.07};
471 inline static constexpr std::array<double, 4> kMinimoogHeadroomMultiplier{1.0, 1.0, 1.30,
473 inline static constexpr std::array<double, 4> kMinimoogCrossMod{0.0, 0.0, 0.26, 0.29};
474 inline static constexpr std::array<double, 4> kMinimoogCutoffDriftCents{0.0, 0.0, 12.0, 13.0};
475 inline static constexpr std::array<double, 4> kMinimoogResonanceDriftFraction{0.0, 0.0, 0.0015,
478 OtaCascadeFilterT<SampleType> engine_;
480 double cutoff_knob_ = 0.5;
481 double cutoff_modulation_octaves_ = 0.0;
482 double resonance_knob_ = 0.0;
483 double drive_db_ = 0.0;
484 double corner_hz_ = 262.0;