diff --git a/src/sfizz.h b/src/sfizz.h index 42585a9d..69faa3ed 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -376,6 +376,24 @@ SFIZZ_EXPORTED_API void sfizz_set_logging_prefix(sfizz_synth_t* synth, const cha */ SFIZZ_EXPORTED_API void sfizz_all_sound_off(sfizz_synth_t* synth); +/** + * @brief Add external definitions prior to loading; + * Note that these do not get reset by loading or resetting the synth. + * You need to call sfizz_clear_external_definitions() to erase them. + * + * @param synth + * @param id + * @param value + */ +SFIZZ_EXPORTED_API void sfizz_add_external_definitions(sfizz_synth_t* synth, const char* id, const char* value); + +/** + * @brief Clears external definitions for the next file loading. + * + * @param synth + */ +SFIZZ_EXPORTED_API void sfizz_clear_external_definitions(sfizz_synth_t* synth); + #ifdef __cplusplus } #endif diff --git a/src/sfizz.hpp b/src/sfizz.hpp index b2af98c1..49e9cd7a 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -296,6 +296,22 @@ public: */ void allSoundOff() noexcept; + /** + * @brief Add external definitions prior to loading; + * Note that these do not get reset by loading or resetting the synth. + * You need to call clearExternalDefintions() to erase them. + * + * @param id + * @param value + */ + void addExternalDefinition(const std::string& id, const std::string& value); + + /** + * @brief Clears external definitions for the next file loading. + * + */ + void clearExternalDefinitions(); + private: std::unique_ptr synth; }; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index a71ad3db..727a125f 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -60,6 +60,7 @@ namespace config { constexpr int filtersInPool { maxVoices * 2 }; constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; + constexpr int oscillatorsPerVoice { 9 }; constexpr float noiseVariance { 0.25f }; /** Minimum interval in frames between recomputations of coefficients of the diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 0e35ca49..158da597 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, config::oscillatorsPerVoice }; + 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..ec102de3 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; @@ -785,7 +785,7 @@ void sfz::Synth::pitchWheel(int delay, int pitch) noexcept { ASSERT(pitch <= 8192); ASSERT(pitch >= -8192); - const auto normalizedPitch = normalizeBend(pitch); + const auto normalizedPitch = normalizeBend(float(pitch)); ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.pitchBendEvent(delay, normalizedPitch); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4aa4e0bd..251a1167 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[config::oscillatorsPerVoice]; + detunes[0] = 0.0; + detunes[1] = -d; + detunes[2] = +d; + for (int i = 3; i < m; ++i) { + int n = (i - 1) / 2; + detunes[i] = d * ((i & 1) ? -0.25f : +0.25f) * float(n); + } + + // 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.0f - float(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 +} diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index f0a70af9..1723c416 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,13 @@ private: ADSREnvelope egEnvelope; float bendStepFactor { centsFactor(1) }; - WavetableOscillator waveOscillator; + WavetableOscillator waveOscillators[config::oscillatorsPerVoice]; + + // unison of oscillators + unsigned waveUnisonSize { 0 }; + float waveDetuneRatio[config::oscillatorsPerVoice] { }; + float waveLeftGain[config::oscillatorsPerVoice] { }; + float waveRightGain[config::oscillatorsPerVoice] { }; 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/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index 08f88c1a..3e17af01 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -210,3 +210,13 @@ void sfz::Sfizz::allSoundOff() noexcept { synth->allSoundOff(); } + +void sfz::Sfizz::addExternalDefinition(const std::string& id, const std::string& value) +{ + synth->getParser().addExternalDefinition(id, value); +} + +void sfz::Sfizz::clearExternalDefinitions() +{ + synth->getParser().clearExternalDefinitions(); +} diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index bc62c4c7..3c9d88f0 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -256,6 +256,18 @@ void sfizz_all_sound_off(sfizz_synth_t* synth) return self->allSoundOff(); } +void sfizz_add_external_definitions(sfizz_synth_t* synth, const char* id, const char* value) +{ + auto self = reinterpret_cast(synth); + self->getParser().addExternalDefinition(id, value); +} + +void sfizz_clear_external_definitions(sfizz_synth_t* synth) +{ + auto self = reinterpret_cast(synth); + self->getParser().clearExternalDefinitions(); +} + #ifdef __cplusplus } #endif 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; diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index f4bdc3c4..51f5e40f 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -254,9 +254,46 @@ TEST_CASE("[Files] Channels (channels.sfz)") synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sample == "mono_sample.wav"); - REQUIRE(!synth.getRegionView(0)->isStereo); + REQUIRE(!synth.getRegionView(0)->isStereo()); REQUIRE(synth.getRegionView(1)->sample == "stereo_sample.wav"); - REQUIRE(synth.getRegionView(1)->isStereo); + REQUIRE(synth.getRegionView(1)->isStereo()); +} + +TEST_CASE("[Files] Channels (channels_multi.sfz)") +{ + sfz::Synth synth; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels_multi.sfz"); + REQUIRE(synth.getNumRegions() == 6); + + REQUIRE(synth.getRegionView(0)->sample == "*sine"); + REQUIRE(!synth.getRegionView(0)->isStereo()); + REQUIRE(synth.getRegionView(0)->isGenerator()); + REQUIRE(!synth.getRegionView(0)->oscillator); + + REQUIRE(synth.getRegionView(1)->sample == "*sine"); + REQUIRE(synth.getRegionView(1)->isStereo()); + REQUIRE(synth.getRegionView(1)->isGenerator()); + REQUIRE(!synth.getRegionView(1)->oscillator); + + REQUIRE(synth.getRegionView(2)->sample == "ramp_wave.wav"); + REQUIRE(!synth.getRegionView(2)->isStereo()); + REQUIRE(!synth.getRegionView(2)->isGenerator()); + REQUIRE(synth.getRegionView(2)->oscillator); + + REQUIRE(synth.getRegionView(3)->sample == "ramp_wave.wav"); + REQUIRE(synth.getRegionView(3)->isStereo()); + REQUIRE(!synth.getRegionView(3)->isGenerator()); + REQUIRE(synth.getRegionView(3)->oscillator); + + REQUIRE(synth.getRegionView(4)->sample == "*sine"); + REQUIRE(!synth.getRegionView(4)->isStereo()); + REQUIRE(synth.getRegionView(4)->isGenerator()); + REQUIRE(!synth.getRegionView(4)->oscillator); + + REQUIRE(synth.getRegionView(5)->sample == "*sine"); + REQUIRE(!synth.getRegionView(5)->isStereo()); + REQUIRE(synth.getRegionView(5)->isGenerator()); + REQUIRE(!synth.getRegionView(5)->oscillator); } TEST_CASE("[Files] sw_default") diff --git a/tests/TestFiles/channels_multi.sfz b/tests/TestFiles/channels_multi.sfz new file mode 100644 index 00000000..8146ba45 --- /dev/null +++ b/tests/TestFiles/channels_multi.sfz @@ -0,0 +1,6 @@ + sample=*sine + sample=*sine oscillator_multi=3 + sample=ramp_wave.wav oscillator=on + sample=ramp_wave.wav oscillator=on oscillator_multi=3 + sample=*sine oscillator_multi=1 + sample=*sine oscillator_multi=2 diff --git a/tests/TestFiles/ramp_wave.wav b/tests/TestFiles/ramp_wave.wav new file mode 100644 index 00000000..838d8556 Binary files /dev/null and b/tests/TestFiles/ramp_wave.wav differ