From 6826024ee2e1f3d14cec3a10194d77fc26f61b28 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Thu, 1 Apr 2021 06:32:42 +0200 Subject: [PATCH] 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); +}