diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 5e1c0094f..ef5da208e 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 cleanOpcode) { - const Opcode opcode = 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 757971cbb..c937c496c 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 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 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 7268ecd8c..fb9ef7eeb 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -896,6 +896,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, false); + } + + regionUpdates_.clear(); +} + void Synth::renderBlock(AudioSpan buffer) noexcept { Impl& impl = *impl_; @@ -925,6 +945,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 81fd6b494..fd5de3fb7 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; @@ -1213,11 +1228,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]) @@ -1285,13 +1295,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]) @@ -1337,10 +1340,73 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co } } 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()); } break; diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 12ca37364..73501323c 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -180,6 +180,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 { @@ -343,6 +349,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/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 3e49b9f59..8a73b80fb 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -223,6 +223,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 }; @@ -472,6 +473,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 || region.velocityOverride == VelocityOverride::previous) @@ -1449,7 +1451,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); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82d1fcf58..013115aa1 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 000000000..e3af3497a --- /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); +}