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.
This commit is contained in:
parent
798f501a65
commit
6826024ee2
7 changed files with 244 additions and 16 deletions
|
|
@ -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) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -888,6 +888,26 @@ void Synth::setSampleRate(float sampleRate) noexcept
|
|||
}
|
||||
}
|
||||
|
||||
void Synth::Impl::updateRegions() noexcept
|
||||
{
|
||||
std::unique_lock<SpinMutex> 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<float> buffer) noexcept
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
|
@ -917,6 +937,8 @@ void Synth::renderBlock(AudioSpan<float> 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);
|
||||
|
|
|
|||
|
|
@ -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<int32_t>(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<SpinMutex> 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<SpinMutex> 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<SpinMutex> 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<SpinMutex> 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());
|
||||
|
|
|
|||
|
|
@ -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<class T>
|
||||
static void collectUsedCCsFromCCMap(BitArray<config::numCCs>& usedCCs, const CCMap<T> 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<OpcodeUpdate> regionUpdates_;
|
||||
SpinMutex regionUpdatesMutex_;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ include(Catch)
|
|||
set(SFIZZ_TEST_SOURCES
|
||||
DirectRegionT.cpp
|
||||
RegionValuesT.cpp
|
||||
RegionValuesSetT.cpp
|
||||
TestHelpers.h
|
||||
TestHelpers.cpp
|
||||
ParsingT.cpp
|
||||
|
|
|
|||
122
tests/RegionValuesSetT.cpp
Normal file
122
tests/RegionValuesSetT.cpp
Normal file
|
|
@ -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 <stdexcept>
|
||||
#include <absl/strings/str_cat.h>
|
||||
#include <absl/strings/str_format.h>
|
||||
using namespace Catch::literals;
|
||||
using namespace sfz;
|
||||
|
||||
TEST_CASE("[Set values] Pitch keycenter")
|
||||
{
|
||||
Synth synth;
|
||||
std::vector<std::string> messageList;
|
||||
Client client(&messageList);
|
||||
client.setReceiveCallback(&simpleMessageReceiver);
|
||||
AudioBuffer<float> buffer { 2, 256 };
|
||||
|
||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"(
|
||||
<region> 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<std::string> expected {
|
||||
"/region0/pitch_keycenter,i : { 48 }",
|
||||
"/region0/pitch_keycenter,i : { 60 }",
|
||||
};
|
||||
REQUIRE(messageList == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[Set values] LFO wave")
|
||||
{
|
||||
Synth synth;
|
||||
std::vector<std::string> messageList;
|
||||
Client client(&messageList);
|
||||
client.setReceiveCallback(&simpleMessageReceiver);
|
||||
AudioBuffer<float> buffer { 2, 256 };
|
||||
|
||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"(
|
||||
<region> 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<std::string> expected {
|
||||
"/region0/lfo0/wave,i : { 5 }",
|
||||
"/region0/lfo0/wave,i : { 2 }",
|
||||
};
|
||||
REQUIRE(messageList == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[Set values] Filter type")
|
||||
{
|
||||
Synth synth;
|
||||
std::vector<std::string> messageList;
|
||||
Client client(&messageList);
|
||||
client.setReceiveCallback(&simpleMessageReceiver);
|
||||
AudioBuffer<float> buffer { 2, 256 };
|
||||
|
||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"(
|
||||
<region> 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<std::string> 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<std::string> messageList;
|
||||
Client client(&messageList);
|
||||
client.setReceiveCallback(&simpleMessageReceiver);
|
||||
AudioBuffer<float> buffer { 2, 256 };
|
||||
|
||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/values_set.sfz", R"(
|
||||
<region> 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<std::string> expected {
|
||||
"/region0/loop_mode,s : { loop_continuous }",
|
||||
"/region0/loop_mode,s : { one_shot }",
|
||||
};
|
||||
REQUIRE(messageList == expected);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue