From 6826024ee2e1f3d14cec3a10194d77fc26f61b28 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Thu, 1 Apr 2021 06:32:42 +0200 Subject: [PATCH 1/2] Updating regions through OSC For a limited selection of opcodes, we can allow updates through OSC. This implements an "opcode buffer" that gets read at the beginning of the render loop in the RT thread. Any opcode updates must thus be RT-safe, so not all of them should be allowed. I added a parameter to disable the opcode name cleanup method which does allocate strings potentially. This means that the opcodes must be "correctly" normalized. For example, for `lfoN_wave`, we have to pass `lfoN_wave1` in the opcode buffer. --- src/sfizz/Region.cpp | 4 +- src/sfizz/Region.h | 3 +- src/sfizz/Synth.cpp | 22 +++++++ src/sfizz/SynthMessaging.cpp | 92 ++++++++++++++++++++++---- src/sfizz/SynthPrivate.h | 16 +++++ tests/CMakeLists.txt | 1 + tests/RegionValuesSetT.cpp | 122 +++++++++++++++++++++++++++++++++++ 7 files changed, 244 insertions(+), 16 deletions(-) create mode 100644 tests/RegionValuesSetT.cpp diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 8ae0805f..aad9ddb8 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -53,9 +53,9 @@ sfz::Region::Region(int regionNumber, absl::string_view defaultPath) case hash(x "_stepcc&"): \ case hash(x "_smoothcc&") -bool sfz::Region::parseOpcode(const Opcode& rawOpcode) +bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool rtSafe) { - const Opcode opcode = rawOpcode.cleanUp(kOpcodeScopeRegion); + const Opcode opcode = rtSafe ? rawOpcode : rawOpcode.cleanUp(kOpcodeScopeRegion); switch (opcode.lettersOnlyHash) { diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 0cfdb921..036915e7 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -186,10 +186,11 @@ struct Region { * This must be called multiple times for each opcode applying to this region. * * @param opcode + * @param rtSafe whether the processing has to be real-time safe * @return true if the opcode was properly read and stored. * @return false */ - bool parseOpcode(const Opcode& opcode); + bool parseOpcode(const Opcode& opcode, bool rtSafe = false); /** * @brief Parse a opcode which is specific to a particular SFZv1 LFO: * amplfo, pitchlfo, fillfo. diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 6304f4d3..7e156a1a 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -888,6 +888,26 @@ void Synth::setSampleRate(float sampleRate) noexcept } } +void Synth::Impl::updateRegions() noexcept +{ + std::unique_lock lock { regionUpdatesMutex_, std::try_to_lock }; + if (!lock.owns_lock()) + return; + + absl::c_sort(regionUpdates_, [](const OpcodeUpdate& lhs, const OpcodeUpdate& rhs) { + return lhs.delay < rhs.delay; + }); + + for (auto& update: regionUpdates_) { + if (!update.region) + continue; + + update.region->parseOpcode(update.opcode, true); + } + + regionUpdates_.clear(); +} + void Synth::renderBlock(AudioSpan buffer) noexcept { Impl& impl = *impl_; @@ -917,6 +937,8 @@ void Synth::renderBlock(AudioSpan buffer) noexcept impl.resources_.filePool.triggerGarbageCollection(); } + impl.updateRegions(); + auto tempSpan = impl.resources_.bufferPool.getStereoBuffer(numFrames); auto tempMixSpan = impl.resources_.bufferPool.getStereoBuffer(numFrames); auto rampSpan = impl.resources_.bufferPool.getBuffer(numFrames); diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 9be0b9ff..9d6ef29c 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -33,6 +33,21 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co Layer& layer = *impl.layers_[idx]; \ const Region& region = layer.getRegion(); + #define GET_FILTER_OR_BREAK(idx) \ + if (idx >= region.filters.size()) \ + break; \ + const auto& filter = region.filters[idx]; + + #define GET_EQ_OR_BREAK(idx) \ + if (idx >= region.equalizers.size()) \ + break; \ + const auto& eq = region.equalizers[idx]; + + #define GET_LFO_OR_BREAK(idx) \ + if (idx >= region.lfos.size()) \ + break; \ + const auto& lfo = region.lfos[idx]; + MATCH("/hello", "") { client.receive(delay, "/hello", "", nullptr); } break; @@ -1161,11 +1176,6 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'f'>(delay, path, value * 100.0f); } break; - #define GET_FILTER_OR_BREAK(idx) \ - if (idx >= region.filters.size()) \ - break; \ - const auto& filter = region.filters[idx]; - MATCH("/region&/filter&/cutoff", "") { GET_REGION_OR_BREAK(indices[0]) GET_FILTER_OR_BREAK(indices[1]) @@ -1233,13 +1243,6 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co } } break; - #undef GET_FILTER_OR_BREAK - - #define GET_EQ_OR_BREAK(idx) \ - if (idx >= region.equalizers.size()) \ - break; \ - const auto& eq = region.equalizers[idx]; - MATCH("/region&/eq&/gain", "") { GET_REGION_OR_BREAK(indices[0]) GET_EQ_OR_BREAK(indices[1]) @@ -1285,9 +1288,72 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co } } break; - #undef GET_EQ_OR_BREAK + MATCH("/region&/lfo&/wave", "") { + GET_REGION_OR_BREAK(indices[0]) + GET_LFO_OR_BREAK(indices[1]) + if (lfo.sub.size() == 0) + break; + + client.receive<'i'>(delay, path, static_cast(lfo.sub[0].wave)); + } break; #undef GET_REGION_OR_BREAK + #undef GET_FILTER_OR_BREAK + #undef GET_EQ_OR_BREAK + #undef GET_LFO_OR_BREAK + + //---------------------------------------------------------------------- + // Setting values + // Note: all these must be rt-safe within the parseOpcode method in region + + #define GET_REGION_OR_BREAK(idx) \ + if (idx >= impl.layers_.size()) \ + break; \ + Layer& layer = *impl.layers_[idx]; \ + Region& region = layer.getRegion(); + + MATCH("/region&/pitch_keycenter", "i") { + GET_REGION_OR_BREAK(indices[0]) + std::lock_guard lock { impl.regionUpdatesMutex_ }; + Impl::OpcodeUpdate update { delay, ®ion, + Opcode { "pitch_keycenter", std::to_string(args[0].i) } }; + impl.regionUpdates_.emplace_back(update); + } break; + + MATCH("/region&/loop_mode", "s") { + GET_REGION_OR_BREAK(indices[0]) + std::lock_guard lock { impl.regionUpdatesMutex_ }; + Impl::OpcodeUpdate update { delay, ®ion, + Opcode { "loop_mode", args[0].s } }; + impl.regionUpdates_.emplace_back(update); + } break; + + MATCH("/region&/filter&/type", "s") { + GET_REGION_OR_BREAK(indices[0]) + if (indices[1] >= region.filters.size()) + break; + + std::lock_guard lock { impl.regionUpdatesMutex_ }; + Impl::OpcodeUpdate update { delay, ®ion, + Opcode { absl::StrCat("fil", indices[1] + 1 , "_type "), args[0].s } }; + impl.regionUpdates_.emplace_back(update); + } break; + + MATCH("/region&/lfo&/wave", "i") { + GET_REGION_OR_BREAK(indices[0]) + if (indices[1] >= region.lfos.size()) + break; + + std::lock_guard lock { impl.regionUpdatesMutex_ }; + Impl::OpcodeUpdate update { delay, ®ion, + Opcode { absl::StrCat("lfo", indices[1] + 1, "_wave1"), std::to_string(args[0].i) } }; + impl.regionUpdates_.emplace_back(update); + } break; + + #undef GET_REGION_OR_BREAK + + //---------------------------------------------------------------------- + // Voices MATCH("/num_active_voices", "") { client.receive<'i'>(delay, path, impl.voiceManager_.getNumActiveVoices()); diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 350a12fd..0dedc22e 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -179,6 +179,12 @@ struct Synth::Impl final: public Parser::Listener { */ void finalizeSfzLoad(); + /** + * @brief Update the regions with the opcodes received through OSC if necessary + * + */ + void updateRegions() noexcept; + template static void collectUsedCCsFromCCMap(BitArray& usedCCs, const CCMap map) noexcept { @@ -327,6 +333,16 @@ struct Synth::Impl final: public Parser::Listener { } bool playheadMoved_ { false }; + + struct OpcodeUpdate + { + int delay; + Region* region; + Opcode opcode; + }; + + std::vector regionUpdates_; + SpinMutex regionUpdatesMutex_; }; } // namespace sfz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82d1fcf5..013115aa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ include(Catch) set(SFIZZ_TEST_SOURCES DirectRegionT.cpp RegionValuesT.cpp + RegionValuesSetT.cpp TestHelpers.h TestHelpers.cpp ParsingT.cpp diff --git a/tests/RegionValuesSetT.cpp b/tests/RegionValuesSetT.cpp new file mode 100644 index 00000000..e3af3497 --- /dev/null +++ b/tests/RegionValuesSetT.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "TestHelpers.h" +#include "sfizz/Synth.h" +#include "catch2/catch.hpp" +#include +#include +#include +using namespace Catch::literals; +using namespace sfz; + +TEST_CASE("[Set values] Pitch keycenter") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"( + sample=*sine pitch_keycenter=48 + )"); + synth.dispatchMessage(client, 0, "/region0/pitch_keycenter", "", nullptr); + + // Update value + sfizz_arg_t args; + args.i = 60; + synth.dispatchMessage(client, 1, "/region0/pitch_keycenter", "i", &args); + synth.renderBlock(buffer); + + synth.dispatchMessage(client, 0, "/region0/pitch_keycenter", "", nullptr); + std::vector expected { + "/region0/pitch_keycenter,i : { 48 }", + "/region0/pitch_keycenter,i : { 60 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Set values] LFO wave") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"( + sample=*sine lfo1_wave=5 + )"); + synth.dispatchMessage(client, 0, "/region0/lfo0/wave", "", nullptr); + + // Update value + sfizz_arg_t args; + args.i = 2; + synth.dispatchMessage(client, 1, "/region0/lfo0/wave", "i", &args); + synth.renderBlock(buffer); + + synth.dispatchMessage(client, 0, "/region0/lfo0/wave", "", nullptr); + std::vector expected { + "/region0/lfo0/wave,i : { 5 }", + "/region0/lfo0/wave,i : { 2 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Set values] Filter type") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"( + sample=*sine fil2_type=lpf_1p + )"); + synth.dispatchMessage(client, 0, "/region0/filter1/type", "", nullptr); + + // Update value + sfizz_arg_t args; + args.s = "hpf_2p"; + synth.dispatchMessage(client, 1, "/region0/filter1/type", "s", &args); + synth.renderBlock(buffer); + + synth.dispatchMessage(client, 0, "/region0/filter1/type", "", nullptr); + std::vector expected { + "/region0/filter1/type,s : { lpf_1p }", + "/region0/filter1/type,s : { hpf_2p }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Set values] Loop mode") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"( + sample=looped_flute.wav + )"); + synth.dispatchMessage(client, 0, "/region0/loop_mode", "", nullptr); + + // Update value + sfizz_arg_t args; + args.s = "one_shot"; + synth.dispatchMessage(client, 1, "/region0/loop_mode", "s", &args); + synth.renderBlock(buffer); + + synth.dispatchMessage(client, 0, "/region0/loop_mode", "", nullptr); + std::vector expected { + "/region0/loop_mode,s : { loop_continuous }", + "/region0/loop_mode,s : { one_shot }", + }; + REQUIRE(messageList == expected); +} From 78db0b73dbaab445f0d1f36e639b53f775f65bf7 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Thu, 1 Apr 2021 06:44:45 +0200 Subject: [PATCH 2/2] Cache the region pitch keycenter at voice start --- src/sfizz/Region.cpp | 4 ++-- src/sfizz/Region.h | 4 ++-- src/sfizz/Synth.cpp | 2 +- src/sfizz/Voice.cpp | 4 +++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index aad9ddb8..a96d646b 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -53,9 +53,9 @@ sfz::Region::Region(int regionNumber, absl::string_view defaultPath) case hash(x "_stepcc&"): \ case hash(x "_smoothcc&") -bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool rtSafe) +bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool cleanOpcode) { - const Opcode opcode = rtSafe ? rawOpcode : rawOpcode.cleanUp(kOpcodeScopeRegion); + const Opcode opcode = cleanOpcode ? rawOpcode.cleanUp(kOpcodeScopeRegion) : rawOpcode; switch (opcode.lettersOnlyHash) { diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 036915e7..522e894b 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -186,11 +186,11 @@ struct Region { * This must be called multiple times for each opcode applying to this region. * * @param opcode - * @param rtSafe whether the processing has to be real-time safe + * @param cleanOpcode whether the opcode should be canonicalized * @return true if the opcode was properly read and stored. * @return false */ - bool parseOpcode(const Opcode& opcode, bool rtSafe = false); + bool parseOpcode(const Opcode& opcode, bool cleanOpcode = true); /** * @brief Parse a opcode which is specific to a particular SFZv1 LFO: * amplfo, pitchlfo, fillfo. diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 7e156a1a..bdf6bafa 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -902,7 +902,7 @@ void Synth::Impl::updateRegions() noexcept if (!update.region) continue; - update.region->parseOpcode(update.opcode, true); + update.region->parseOpcode(update.opcode, false); } regionUpdates_.clear(); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 1f449bd0..69b52eea 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -222,6 +222,7 @@ struct Voice::Impl float baseVolumedB_ { 0.0 }; float baseGain_ { 1.0 }; float baseFrequency_ { 440.0 }; + uint8_t pitchKeycenter_ { Default::key }; float floatPositionOffset_ { 0.0f }; int sourcePosition_ { 0 }; @@ -467,6 +468,7 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc if (resources.stretch) impl.pitchRatio_ *= resources.stretch->getRatioForFractionalKey(numberRetuned); + impl.pitchKeycenter_ = region.pitchKeycenter; impl.baseVolumedB_ = region.getBaseVolumedB(resources.midiState, impl.triggerEvent_.number); impl.baseGain_ = region.getBaseGain(); if (impl.triggerEvent_.type != TriggerEventType::CC) @@ -1425,7 +1427,7 @@ void Voice::Impl::fillWithGenerator(AudioSpan buffer) noexcept if (!frequencies) return; - float keycenterFrequency = midiNoteFrequency(region_->pitchKeycenter); + float keycenterFrequency = midiNoteFrequency(pitchKeycenter_); fill(*frequencies, pitchRatio_ * keycenterFrequency); pitchEnvelope(*frequencies);