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:
Paul Fd 2021-04-01 06:32:42 +02:00
parent 798f501a65
commit 6826024ee2
7 changed files with 244 additions and 16 deletions

View file

@ -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) {

View file

@ -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.

View file

@ -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);

View file

@ -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, &region,
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, &region,
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, &region,
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, &region,
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());

View file

@ -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

View file

@ -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
View 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);
}