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); +}