From 4acb4cd7f7959728533b40c56109229e5d966ec9 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 22 Jun 2020 05:25:48 +0200 Subject: [PATCH 1/5] Add a lookup table for wavetable octaves --- src/sfizz/Wavetables.cpp | 33 +++++++++++++++++++++++++++++++++ src/sfizz/Wavetables.h | 1 + tests/WavetablesT.cpp | 16 ++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index e8995b69..d13a8e7c 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -200,6 +200,39 @@ unsigned WavetableRange::getOctaveForFrequency(float f) return clamp(oct, 0, countOctaves - 1); } +static const auto octaveForFrequencyTable = []() +{ + static constexpr unsigned N = 1024; + std::array table; + + constexpr double fmin = 1 / WavetableRange::frequencyScaleFactor; + constexpr double fmax = (1 << (WavetableRange::countOctaves - 1)) / WavetableRange::frequencyScaleFactor; + + for (unsigned i = 0; i < N; ++i) { + double f = fmin + (i * (1.0 / (N - 1))) * (fmax - fmin); + table[i] = std::log2(f * WavetableRange::frequencyScaleFactor); + } + + return table; +}(); + +float WavetableRange::getFractionalOctaveForFrequency(float f) +{ + static constexpr unsigned N = octaveForFrequencyTable.size(); + + constexpr double fmin = 1 / WavetableRange::frequencyScaleFactor; + constexpr double fmax = (1 << (WavetableRange::countOctaves - 1)) / WavetableRange::frequencyScaleFactor; + + float pos = (f - fmin) * ((N - 1) / static_cast(fmax - fmin)); + int index1 = static_cast(pos); + float frac = pos - index1; + index1 = clamp(index1, 0, N - 1); + int index2 = std::min(index1 + 1, N - 1); + + return (1.0f - frac) * octaveForFrequencyTable[index1] + + frac * octaveForFrequencyTable[index2]; +} + WavetableRange WavetableRange::getRangeForOctave(int o) { WavetableRange range; diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index 3d0c9039..e5a5df36 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -113,6 +113,7 @@ public: static constexpr float frequencyScaleFactor = 0.05; static unsigned getOctaveForFrequency(float f); + static float getFractionalOctaveForFrequency(float f); static WavetableRange getRangeForOctave(int o); static WavetableRange getRangeForFrequency(float f); diff --git a/tests/WavetablesT.cpp b/tests/WavetablesT.cpp index 51231614..c4d5b683 100644 --- a/tests/WavetablesT.cpp +++ b/tests/WavetablesT.cpp @@ -8,6 +8,7 @@ #include "sfizz/MathHelpers.h" #include "catch2/catch.hpp" #include +#include TEST_CASE("[Wavetables] Frequency ranges") { @@ -38,3 +39,18 @@ TEST_CASE("[Wavetables] Frequency ranges") REQUIRE(min_oct == 0); REQUIRE(max_oct == sfz::WavetableRange::countOctaves - 1); } + +TEST_CASE("[Wavetables] Octave number lookup") +{ + for (int note = 0; note < 128; ++note) { + double f = midiNoteFrequency(note); + + float ref = std::log2(f * sfz::WavetableRange::frequencyScaleFactor); + float oct = sfz::WavetableRange::getFractionalOctaveForFrequency(f); + + ref = clamp(ref, 0, sfz::WavetableRange::countOctaves - 1); + oct = clamp(oct, 0, sfz::WavetableRange::countOctaves - 1); + + REQUIRE(oct == Approx(ref).margin(0.03f)); + } +} From c9b46876611075d9e673fef6de78bf624b82bbb3 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 22 Jun 2020 06:24:39 +0200 Subject: [PATCH 2/5] Add support of various wavetable quality settings --- src/sfizz/Wavetables.cpp | 123 +++++++++++++++++++++++++++++++++++---- src/sfizz/Wavetables.h | 58 +++++++++++++++--- 2 files changed, 164 insertions(+), 17 deletions(-) diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index d13a8e7c..929735a1 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -6,6 +6,7 @@ #include "Wavetables.h" #include "FilePool.h" +#include "Interpolators.h" #include "MathHelpers.h" #include "absl/meta/type_traits.h" #include @@ -35,7 +36,8 @@ void WavetableOscillator::setPhase(float phase) _phase = phase; } -void WavetableOscillator::process(float frequency, float detuneRatio, float* output, unsigned nframes) +template +void WavetableOscillator::processSingle(float frequency, float detuneRatio, float* output, unsigned nframes) { float phase = _phase; float phaseInc = frequency * (detuneRatio * _sampleInterval); @@ -48,7 +50,7 @@ void WavetableOscillator::process(float frequency, float detuneRatio, float* out float position = phase * tableSize; unsigned index = static_cast(position); float frac = position - index; - output[i] = interpolate(&table[index], frac); + output[i] = interpolate(&table[index], frac); phase += phaseInc; phase -= static_cast(phase); @@ -57,7 +59,8 @@ void WavetableOscillator::process(float frequency, float detuneRatio, float* out _phase = phase; } -void WavetableOscillator::processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes) +template +void WavetableOscillator::processModulatedSingle(const float* frequencies, float detuneRatio, float* output, unsigned nframes) { float phase = _phase; float sampleInterval = _sampleInterval; @@ -73,7 +76,7 @@ void WavetableOscillator::processModulated(const float* frequencies, float detun float position = phase * tableSize; unsigned index = static_cast(position); float frac = position - index; - output[i] = interpolate(&table[index], frac); + output[i] = interpolate(&table[index], frac); phase += phaseInc; phase -= static_cast(phase); @@ -82,9 +85,96 @@ void WavetableOscillator::processModulated(const float* frequencies, float detun _phase = phase; } -float WavetableOscillator::interpolate(const float* x, float delta) +template +void WavetableOscillator::processDual(float frequency, float detuneRatio, float* output, unsigned nframes) { - return x[0] + delta * (x[1] - x[0]); + float phase = _phase; + float phaseInc = frequency * (detuneRatio * _sampleInterval); + + const WavetableMulti& multi = *_multi; + unsigned tableSize = multi.tableSize(); + WavetableMulti::DualTable dt = multi.getInterpolationPairForFrequency(frequency); + + for (unsigned i = 0; i < nframes; ++i) { + float position = phase * tableSize; + unsigned index = static_cast(position); + float frac = position - index; + output[i] = + (1 - dt.delta) * interpolate(&dt.table1[index], frac) + + dt.delta * interpolate(&dt.table2[index], frac); + + phase += phaseInc; + phase -= static_cast(phase); + } + + _phase = phase; +} + +template +void WavetableOscillator::processModulatedDual(const float* frequencies, float detuneRatio, float* output, unsigned nframes) +{ + float phase = _phase; + float sampleInterval = _sampleInterval; + + const WavetableMulti& multi = *_multi; + unsigned tableSize = multi.tableSize(); + + for (unsigned i = 0; i < nframes; ++i) { + float frequency = frequencies[i]; + float phaseInc = frequency * (detuneRatio * sampleInterval); + + WavetableMulti::DualTable dt = multi.getInterpolationPairForFrequency(frequency); + + float position = phase * tableSize; + unsigned index = static_cast(position); + float frac = position - index; + output[i] = + (1 - dt.delta) * interpolate(&dt.table1[index], frac) + + dt.delta * interpolate(&dt.table2[index], frac); + + phase += phaseInc; + phase -= static_cast(phase); + } + + _phase = phase; +} + +void WavetableOscillator::process(float frequency, float detuneRatio, float* output, unsigned nframes) +{ + int quality = clamp(_quality, 0, 3); + + switch (quality) { + case 0: // supposed to be nearest according to book + // fall through + case 1: + processSingle(frequency, detuneRatio, output, nframes); + break; + case 2: + processSingle(frequency, detuneRatio, output, nframes); + break; + case 3: + processDual(frequency, detuneRatio, output, nframes); + break; + } +} + +void WavetableOscillator::processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes) +{ + int quality = clamp(_quality, 0, 3); + + switch (quality) { + case 0: // supposed to be nearest according to book + // fall through + case 1: + processModulatedSingle(frequencies, detuneRatio, output, nframes); + break; + case 2: + processModulatedSingle(frequencies, detuneRatio, output, nframes); + break; + case 3: + processModulatedDual(frequencies, detuneRatio, output, nframes); + break; + } } //------------------------------------------------------------------------------ @@ -304,7 +394,7 @@ const WavetableMulti* WavetableMulti::getSilenceWavetable() void WavetableMulti::allocateStorage(unsigned tableSize) { - _multiData.resize((tableSize + _tableExtra) * numTables()); + _multiData.resize((tableSize + 2 * _tableExtra) * numTables()); _tableSize = tableSize; } @@ -315,9 +405,22 @@ void WavetableMulti::fillExtra() constexpr unsigned numTables = WavetableMulti::numTables(); for (unsigned m = 0; m < numTables; ++m) { - float* ptr = const_cast(getTablePointer(m)); - for (unsigned i = 0; i < tableExtra; ++i) - ptr[tableSize + i] = ptr[i % tableSize]; + float* beg = const_cast(getTablePointer(m)); + float* end = beg + tableSize; + // fill right + float* src = beg; + float* dst = end; + for (unsigned i = 0; i < tableExtra; ++i) { + *dst++ = *src; + src = (src + 1 != end) ? (src + 1) : beg; + } + // fill left + src = end - 1; + dst = beg - 1; + for (unsigned i = 0; i < tableExtra; ++i) { + *dst-- = *src; + src = (src != beg) ? (src - 1) : (end - 1); + } } } diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index e5a5df36..e2f26cfb 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -8,6 +8,7 @@ #include "Config.h" #include "LeakDetector.h" #include "Buffer.h" +#include "MathHelpers.h" #include #include #include @@ -18,6 +19,8 @@ class FilePool; class WavetableMulti; +enum InterpolatorModel : int; + /** An oscillator based on wavetables */ @@ -44,6 +47,16 @@ public: */ void setPhase(float phase); + /** + Set the quality of this oscillator. (cf. `oscillator_quality`) + + 0: nearest + 1: linear + 2: high + 3: dual-high + */ + void setQuality(int q) { _quality = q; } + /** Compute a cycle of the oscillator, with constant frequency. */ @@ -55,17 +68,23 @@ public: void processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes); private: - /** - Interpolate a value from a part of table, with delta in 0 to 1 excluded. - There are `TableExtra` elements available for reading. - (cf. WavetableMulti) - */ - static float interpolate(const float* x, float delta); + // single-table interpolation + template + void processSingle(float frequency, float detuneRatio, float* output, unsigned nframes); + template + void processModulatedSingle(const float* frequencies, float detuneRatio, float* output, unsigned nframes); + + // dual-table interpolation + template + void processDual(float frequency, float detuneRatio, float* output, unsigned nframes); + template + void processModulatedDual(const float* frequencies, float detuneRatio, float* output, unsigned nframes); private: float _phase = 0.0f; float _sampleInterval = 0.0f; const WavetableMulti* _multi = nullptr; + int _quality = 1; LEAK_DETECTOR(WavetableOscillator); }; @@ -154,6 +173,31 @@ public: return getTable(WavetableRange::getOctaveForFrequency(freq)); } + // adjacent tables with interpolation factor between them + struct DualTable { + const float* table1; + const float* table2; + float delta; + }; + + // get the pair of tables at the fractional multisample position (range checked) + DualTable getInterpolationPair(float position) const + { + DualTable dt; + int index = static_cast(position); + dt.delta = position - index; + dt.table1 = getTablePointer(clamp(index, 0, WavetableRange::countOctaves - 1)); + dt.table2 = getTablePointer(clamp(index + 1, 0, WavetableRange::countOctaves - 1)); + return dt; + } + + // get the pair of tables for the given playback frequency (range checked) + DualTable getInterpolationPairForFrequency(float freq) const + { + float position = WavetableRange::getFractionalOctaveForFrequency(freq); + return getInterpolationPair(position); + } + // create a multisample according to a given harmonic profile // the reference sample rate is the minimum value accepted by the DSP // system (most defavorable wrt. aliasing) @@ -167,7 +211,7 @@ private: // get a pointer to the beginning of the N-th table const float* getTablePointer(unsigned index) const { - return _multiData.data() + index * (_tableSize + _tableExtra); + return _multiData.data() + index * (_tableSize + 2 * _tableExtra) + _tableExtra; } // allocate the internal data for tables of the given size From 80736aec991fe6af7fa43e71bdda5e5d2580e978 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 22 Jun 2020 06:35:31 +0200 Subject: [PATCH 3/5] Support the opcode oscillator_quality --- src/sfizz/Defaults.h | 2 ++ src/sfizz/Region.cpp | 6 ++++++ src/sfizz/Region.h | 1 + src/sfizz/Voice.cpp | 4 ++++ 4 files changed, 13 insertions(+) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 78410f8a..f5edae7f 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -67,6 +67,8 @@ namespace Default constexpr Range oscillatorMultiRange { 1, config::oscillatorsPerVoice }; constexpr float oscillatorDetune { 0 }; constexpr Range oscillatorDetuneRange { -9600, 9600 }; + constexpr int oscillatorQuality { 1 }; + constexpr Range oscillatorQualityRange { 0, 3 }; // Instrument setting: voice lifecycle constexpr uint32_t group { 0 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 47cf33bd..287fc7ad 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -135,6 +135,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("oscillator_detune"): setValueFromOpcode(opcode, oscillatorDetune, Default::oscillatorDetuneRange); break; + case hash("oscillator_quality"): + if (opcode.value == "-1") + oscillatorQuality.reset(); + else + setValueFromOpcode(opcode, oscillatorQuality, Default::oscillatorQualityRange); + break; // Instrument settings: voice lifecycle case hash("group"): // also polyphony_group diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index c346949e..de80f53e 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -276,6 +276,7 @@ struct Region { bool oscillator = false; int oscillatorMulti = Default::oscillatorMulti; float oscillatorDetune = Default::oscillatorDetune; + absl::optional oscillatorQuality; // Instrument settings: voice lifecycle uint32_t group { Default::group }; // group diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 7e384345..53066593 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -65,16 +65,20 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, wave = resources.wavePool.getWaveSaw(); break; } + const int quality = region->oscillatorQuality ? *region->oscillatorQuality : Default::oscillatorQuality; for (WavetableOscillator& osc : waveOscillators) { osc.setWavetable(wave); osc.setPhase(region->getPhase()); + osc.setQuality(quality); } setupOscillatorUnison(); } else if (region->oscillator) { const WavetableMulti* wave = resources.wavePool.getFileWave(region->sampleId.filename()); + const int quality = region->oscillatorQuality ? *region->oscillatorQuality : Default::oscillatorQuality; for (WavetableOscillator& osc : waveOscillators) { osc.setWavetable(wave); osc.setPhase(region->getPhase()); + osc.setQuality(quality); } setupOscillatorUnison(); } else { From 3cd56b4697d27c43b48ff3bb8ecfc12576219c28 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 22 Jun 2020 06:39:06 +0200 Subject: [PATCH 4/5] Support interpolation to nearest --- src/sfizz/Interpolators.h | 2 ++ src/sfizz/Interpolators.hpp | 13 +++++++++++++ src/sfizz/Wavetables.cpp | 10 ++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Interpolators.h b/src/sfizz/Interpolators.h index f784eefd..c4fdbbd9 100644 --- a/src/sfizz/Interpolators.h +++ b/src/sfizz/Interpolators.h @@ -9,6 +9,8 @@ namespace sfz { enum InterpolatorModel : int { + // a nearest interpolator + kInterpolatorNearest, // a linear interpolator kInterpolatorLinear, // a Hermite 3rd order interpolator diff --git a/src/sfizz/Interpolators.hpp b/src/sfizz/Interpolators.hpp index 1f462578..5c302900 100644 --- a/src/sfizz/Interpolators.hpp +++ b/src/sfizz/Interpolators.hpp @@ -19,6 +19,19 @@ inline R interpolate(const R* values, R coeff) return Interpolator::process(values, coeff); } +//------------------------------------------------------------------------------ +// Nearest + +template +class Interpolator +{ +public: + static inline R process(const R* values, R coeff) + { + return values[coeff > static_cast(0.5)]; + } +}; + //------------------------------------------------------------------------------ // Linear diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index 929735a1..e294cfee 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -144,8 +144,9 @@ void WavetableOscillator::process(float frequency, float detuneRatio, float* out int quality = clamp(_quality, 0, 3); switch (quality) { - case 0: // supposed to be nearest according to book - // fall through + case 0: + processSingle(frequency, detuneRatio, output, nframes); + break; case 1: processSingle(frequency, detuneRatio, output, nframes); break; @@ -163,8 +164,9 @@ void WavetableOscillator::processModulated(const float* frequencies, float detun int quality = clamp(_quality, 0, 3); switch (quality) { - case 0: // supposed to be nearest according to book - // fall through + case 0: + processModulatedSingle(frequencies, detuneRatio, output, nframes); + break; case 1: processModulatedSingle(frequencies, detuneRatio, output, nframes); break; From 06595c0ae64fdbe45962a28798cdd84135061709 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 22 Jun 2020 21:58:34 +0200 Subject: [PATCH 5/5] Minor clean up for oscillator initialization --- src/sfizz/Voice.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 53066593..75374065 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -65,19 +65,21 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, wave = resources.wavePool.getWaveSaw(); break; } - const int quality = region->oscillatorQuality ? *region->oscillatorQuality : Default::oscillatorQuality; + const float phase = region->getPhase(); + const int quality = region->oscillatorQuality.value_or(Default::oscillatorQuality); for (WavetableOscillator& osc : waveOscillators) { osc.setWavetable(wave); - osc.setPhase(region->getPhase()); + osc.setPhase(phase); osc.setQuality(quality); } setupOscillatorUnison(); } else if (region->oscillator) { const WavetableMulti* wave = resources.wavePool.getFileWave(region->sampleId.filename()); - const int quality = region->oscillatorQuality ? *region->oscillatorQuality : Default::oscillatorQuality; + const float phase = region->getPhase(); + const int quality = region->oscillatorQuality.value_or(Default::oscillatorQuality); for (WavetableOscillator& osc : waveOscillators) { osc.setWavetable(wave); - osc.setPhase(region->getPhase()); + osc.setPhase(phase); osc.setQuality(quality); } setupOscillatorUnison();