Merge pull request #761 from paulfd/osc-region-change
Updating regions through OSC
This commit is contained in:
commit
254d00768e
8 changed files with 247 additions and 17 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 cleanOpcode)
|
||||
{
|
||||
const Opcode opcode = rawOpcode.cleanUp(kOpcodeScopeRegion);
|
||||
const Opcode opcode = cleanOpcode ? rawOpcode.cleanUp(kOpcodeScopeRegion) : rawOpcode;
|
||||
|
||||
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 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.
|
||||
|
|
|
|||
|
|
@ -896,6 +896,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, false);
|
||||
}
|
||||
|
||||
regionUpdates_.clear();
|
||||
}
|
||||
|
||||
void Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
|
|
@ -925,6 +945,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;
|
||||
|
|
@ -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,9 +1340,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());
|
||||
|
|
|
|||
|
|
@ -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<class T>
|
||||
static void collectUsedCCsFromCCMap(BitArray<config::numCCs>& usedCCs, const CCMap<T> 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<OpcodeUpdate> regionUpdates_;
|
||||
SpinMutex regionUpdatesMutex_;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
|
|
|
|||
|
|
@ -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<float> buffer) noexcept
|
|||
if (!frequencies)
|
||||
return;
|
||||
|
||||
float keycenterFrequency = midiNoteFrequency(region_->pitchKeycenter);
|
||||
float keycenterFrequency = midiNoteFrequency(pitchKeycenter_);
|
||||
fill(*frequencies, pitchRatio_ * keycenterFrequency);
|
||||
pitchEnvelope(*frequencies);
|
||||
|
||||
|
|
|
|||
|
|
@ -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