From cea3e4715beea6b755cf50e71dcd0ebee47ad203 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 3 Apr 2020 15:37:47 +0200 Subject: [PATCH] Add oscillator unison --- src/sfizz/Defaults.h | 4 ++ src/sfizz/Range.h | 4 +- src/sfizz/Region.cpp | 6 +++ src/sfizz/Region.h | 11 +++- src/sfizz/Synth.cpp | 2 +- src/sfizz/Voice.cpp | 109 ++++++++++++++++++++++++++++++++++----- src/sfizz/Voice.h | 14 ++++- src/sfizz/Wavetables.cpp | 8 +-- src/sfizz/Wavetables.h | 4 +- tests/DemoWavetables.cpp | 2 +- 10 files changed, 139 insertions(+), 25 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 0e35ca49..2066cb19 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -63,6 +63,10 @@ namespace Default // Wavetable oscillator constexpr float oscillatorPhase { 0.0 }; constexpr Range oscillatorPhaseRange { -1.0, 360.0 }; + constexpr int oscillatorMulti { 1 }; + constexpr Range oscillatorMultiRange { 1, 9 }; + constexpr float oscillatorDetune { 0 }; + constexpr Range oscillatorDetuneRange { -9600, 9600 }; // Instrument setting: voice lifecycle constexpr uint32_t group { 0 }; diff --git a/src/sfizz/Range.h b/src/sfizz/Range.h index b39b7c0d..9f339565 100644 --- a/src/sfizz/Range.h +++ b/src/sfizz/Range.h @@ -28,8 +28,8 @@ public: { } - Type getStart() const noexcept { return _start; } - Type getEnd() const noexcept { return _end; } + constexpr Type getStart() const noexcept { return _start; } + constexpr Type getEnd() const noexcept { return _end; } /** * @brief Get the range as an std::pair of the endpoints * diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 76806b48..234878e9 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -102,6 +102,12 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) if (auto value = readBooleanFromOpcode(opcode)) oscillator = *value; break; + case hash("oscillator_multi"): + setValueFromOpcode(opcode, oscillatorMulti, Default::oscillatorMultiRange); + break; + case hash("oscillator_detune"): + setValueFromOpcode(opcode, oscillatorDetune, Default::oscillatorDetuneRange); + break; // Instrument settings: voice lifecycle case hash("group"): // fallthrough diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index a7a4e710..73f3bc53 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -60,6 +60,13 @@ struct Region { * @return false */ bool isGenerator() const noexcept { return sample.size() > 0 ? sample[0] == '*' : false; } + /** + * @brief Is stereo (has stereo sample or is unison oscillator)? + * + * @return true + * @return false + */ + bool isStereo() const noexcept { return hasStereoSample || ((oscillator || isGenerator()) && oscillatorMulti >= 3); } /** * @brief Is a looping region (at least potentially)? * @@ -235,6 +242,8 @@ struct Region { // Wavetable oscillator float oscillatorPhase { Default::oscillatorPhase }; bool oscillator = false; + int oscillatorMulti = Default::oscillatorMulti; + float oscillatorDetune = Default::oscillatorDetune; // Instrument settings: voice lifecycle uint32_t group { Default::group }; // group @@ -318,7 +327,7 @@ struct Region { EGDescription pitchEG; EGDescription filterEG; - bool isStereo { false }; + bool hasStereoSample { false }; // Effects std::vector gainToEffect; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 25393b4d..54d7fc12 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -379,7 +379,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) } if (fileInformation->numChannels == 2) - region->isStereo = true; + region->hasStereoSample = true; // TODO: adjust with LFO targets const auto maxOffset = region->offset + region->offsetRandom; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4aa4e0bd..775d220d 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -21,7 +21,8 @@ sfz::Voice::Voice(sfz::Resources& resources) filters.reserve(config::filtersPerVoice); equalizers.reserve(config::eqsPerVoice); - waveOscillator.init(sampleRate); + for (WavetableOscillator& osc : waveOscillators) + osc.init(sampleRate); } void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept @@ -59,12 +60,18 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, wave = resources.wavePool.getWaveSaw(); break; } - waveOscillator.setWavetable(wave); - waveOscillator.setPhase(region->getPhase()); + for (WavetableOscillator& osc : waveOscillators) { + osc.setWavetable(wave); + osc.setPhase(region->getPhase()); + } + setupOscillatorUnison(); } else if (region->oscillator) { const WavetableMulti* wave = resources.wavePool.getFileWave(region->sample); - waveOscillator.setWavetable(wave); - waveOscillator.setPhase(region->getPhase()); + for (WavetableOscillator& osc : waveOscillators) { + osc.setWavetable(wave); + osc.setPhase(region->getPhase()); + } + setupOscillatorUnison(); } else { currentPromise = resources.filePool.getFilePromise(region->sample); if (currentPromise == nullptr) { @@ -83,7 +90,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, ASSERT((filters.capacity() - filters.size()) >= region->filters.size()); ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size()); - const unsigned numChannels = region->isStereo ? 2 : 1; + const unsigned numChannels = region->isStereo() ? 2 : 1; for (auto& filter: region->filters) { auto newFilter = resources.filterPool.getFilter(filter, numChannels, number, value); if (newFilter) @@ -187,7 +194,8 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept { this->sampleRate = sampleRate; - waveOscillator.init(sampleRate); + for (WavetableOscillator& osc : waveOscillators) + osc.init(sampleRate); } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept @@ -218,7 +226,7 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept fillWithData(delayed_buffer); } - if (region->isStereo) { + if (region->isStereo()) { ampStageStereo(buffer); panStageStereo(buffer); filterStageStereo(buffer); @@ -518,9 +526,10 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept absl::c_generate(leftSpan, [&](){ return noiseDist(Random::randomGenerator); }); absl::c_generate(rightSpan, [&](){ return noiseDist(Random::randomGenerator); }); } else { - const auto numSamples = buffer.getNumFrames(); - auto frequencies = resources.bufferPool.getBuffer(numSamples); - auto bends = resources.bufferPool.getBuffer(numSamples); + const auto numFrames = buffer.getNumFrames(); + + auto frequencies = resources.bufferPool.getBuffer(numFrames); + auto bends = resources.bufferPool.getBuffer(numFrames); if (!frequencies || !bends) return; @@ -544,8 +553,25 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept applyGain(*bends, *frequencies); } - waveOscillator.processModulated(frequencies->data(), leftSpan.data(), buffer.getNumFrames()); - copy(leftSpan, rightSpan); + if (waveUnisonSize == 1) { + WavetableOscillator& osc = waveOscillators[0]; + osc.processModulated(frequencies->data(), 1.0, leftSpan.data(), buffer.getNumFrames()); + copy(leftSpan, rightSpan); + } + else { + buffer.fill(0.0f); + + auto tempSpan = resources.bufferPool.getBuffer(numFrames); + if (!tempSpan) + return; + + for (unsigned i = 0, n = waveUnisonSize; i < n; ++i) { + WavetableOscillator& osc = waveOscillators[i]; + osc.processModulated(frequencies->data(), waveDetuneRatio[i], tempSpan->data(), numFrames); + sfz::multiplyAdd(waveLeftGain[i], *tempSpan, leftSpan); + sfz::multiplyAdd(waveRightGain[i], *tempSpan, rightSpan); + } + } } } @@ -620,3 +646,60 @@ void sfz::Voice::setMaxEQsPerVoice(size_t numFilters) ASSERT(equalizers.size() == 0); equalizers.reserve(numFilters); } + +void sfz::Voice::setupOscillatorUnison() +{ + int m = region->oscillatorMulti; + float d = region->oscillatorDetune; + + // 3-9: unison mode, 1: normal/RM, 2: PM/FM + // TODO(jpc) RM/FM/PM synthesis + if (m < 3) { + waveUnisonSize = 1; + waveDetuneRatio[0] = 1.0; + waveLeftGain[0] = 1.0; + waveRightGain[0] = 1.0; + return; + } + + // oscillator count, aka. unison size + waveUnisonSize = m; + + // detune (cents) + float detunes[maxWaveOscillators]; + detunes[0] = 0.0; + detunes[1] = -d; + detunes[2] = +d; + for (int i = 3; i < m; ++i) + detunes[i] = d * ((i & 1) ? -0.25f : +0.25f) * ((i - 1) / 2); + + // detune (ratio) + for (int i = 0; i < m; ++i) + waveDetuneRatio[i] = std::exp2(detunes[i] * (0.01f / 12.0f)); + + // gains + waveLeftGain[0] = 0.0; + waveRightGain[m - 1] = 0.0; + for (int i = 0; i < m - 1; ++i) { + float g = 1 - i / float(m - 1); + waveLeftGain[m - 1 - i] = g; + waveRightGain[i] = g; + } + +#if 0 + fprintf(stderr, "\n"); + fprintf(stderr, "# Left:\n"); + for (int i = m - 1; i >= 0; --i) { + if (waveLeftGain[i] != 0) + fprintf(stderr, "[%d] %10g cents, %10g dB\n", i, detunes[i], 20.0f * std::log10(waveLeftGain[i])); + } + fprintf(stderr, "\n"); + fprintf(stderr, "# Right:\n"); + for (int i = 0; i < m; ++i) { + if (waveRightGain[i] != 0) + fprintf(stderr, "[%d] %10g cents, %10g dB\n", i, detunes[i], 20.0f * std::log10(waveRightGain[i])); + } +#endif +} + +constexpr unsigned sfz::Voice::maxWaveOscillators; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index f0a70af9..afde9b53 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -244,6 +244,10 @@ private: void panStageStereo(AudioSpan buffer) noexcept; void filterStageMono(AudioSpan buffer) noexcept; void filterStageStereo(AudioSpan buffer) noexcept; + /** + * @brief Initialize frequency and gain coefficients for the oscillators. + */ + void setupOscillatorUnison(); Region* region { nullptr }; @@ -284,7 +288,15 @@ private: ADSREnvelope egEnvelope; float bendStepFactor { centsFactor(1) }; - WavetableOscillator waveOscillator; + static constexpr unsigned maxWaveOscillators = Default::oscillatorMultiRange.getEnd(); + + WavetableOscillator waveOscillators[maxWaveOscillators]; + + // unison of oscillators + unsigned waveUnisonSize { 0 }; + float waveDetuneRatio[maxWaveOscillators] { }; + float waveLeftGain[maxWaveOscillators] { }; + float waveRightGain[maxWaveOscillators] { }; Duration dataDuration; Duration amplitudeDuration; diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index c65b244a..b04c0a8c 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -34,10 +34,10 @@ void WavetableOscillator::setPhase(float phase) _phase = phase; } -void WavetableOscillator::process(float frequency, float* output, unsigned nframes) +void WavetableOscillator::process(float frequency, float detuneRatio, float* output, unsigned nframes) { float phase = _phase; - float phaseInc = frequency * _sampleInterval; + float phaseInc = frequency * (detuneRatio * _sampleInterval); const WavetableMulti& multi = *_multi; unsigned tableSize = multi.tableSize(); @@ -56,7 +56,7 @@ void WavetableOscillator::process(float frequency, float* output, unsigned nfram _phase = phase; } -void WavetableOscillator::processModulated(const float* frequencies, float* output, unsigned nframes) +void WavetableOscillator::processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes) { float phase = _phase; float sampleInterval = _sampleInterval; @@ -66,7 +66,7 @@ void WavetableOscillator::processModulated(const float* frequencies, float* outp for (unsigned i = 0; i < nframes; ++i) { float frequency = frequencies[i]; - float phaseInc = frequency * sampleInterval; + float phaseInc = frequency * (detuneRatio * sampleInterval); absl::Span table = multi.getTableForFrequency(frequency); float position = phase * tableSize; diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index 6290f01f..3d0c9039 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -47,12 +47,12 @@ public: /** Compute a cycle of the oscillator, with constant frequency. */ - void process(float frequency, float* output, unsigned nframes); + void process(float frequency, float detuneRatio, float* output, unsigned nframes); /** Compute a cycle of the oscillator, with varying frequency. */ - void processModulated(const float* frequencies, float* output, unsigned nframes); + void processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes); private: /** diff --git a/tests/DemoWavetables.cpp b/tests/DemoWavetables.cpp index d5b9ba5a..e573260b 100644 --- a/tests/DemoWavetables.cpp +++ b/tests/DemoWavetables.cpp @@ -172,7 +172,7 @@ int DemoApp::processAudio(jack_nframes_t nframes, void* cbdata) self->fSweepCurrent = sweepCurrent; // compute oscillator - osc.processModulated(frequency, left, nframes); + osc.processModulated(frequency, 1.0, left, nframes); std::memcpy(right, left, nframes * sizeof(float)); return 0;