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 "_stepcc&"): \
|
||||||
case hash(x "_smoothcc&")
|
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) {
|
switch (opcode.lettersOnlyHash) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,10 +186,11 @@ struct Region {
|
||||||
* This must be called multiple times for each opcode applying to this region.
|
* This must be called multiple times for each opcode applying to this region.
|
||||||
*
|
*
|
||||||
* @param opcode
|
* @param opcode
|
||||||
|
* @param cleanOpcode whether the opcode should be canonicalized
|
||||||
* @return true if the opcode was properly read and stored.
|
* @return true if the opcode was properly read and stored.
|
||||||
* @return false
|
* @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:
|
* @brief Parse a opcode which is specific to a particular SFZv1 LFO:
|
||||||
* amplfo, pitchlfo, fillfo.
|
* 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
|
void Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
{
|
{
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
|
|
@ -925,6 +945,8 @@ void Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
impl.resources_.filePool.triggerGarbageCollection();
|
impl.resources_.filePool.triggerGarbageCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl.updateRegions();
|
||||||
|
|
||||||
auto tempSpan = impl.resources_.bufferPool.getStereoBuffer(numFrames);
|
auto tempSpan = impl.resources_.bufferPool.getStereoBuffer(numFrames);
|
||||||
auto tempMixSpan = impl.resources_.bufferPool.getStereoBuffer(numFrames);
|
auto tempMixSpan = impl.resources_.bufferPool.getStereoBuffer(numFrames);
|
||||||
auto rampSpan = impl.resources_.bufferPool.getBuffer(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]; \
|
Layer& layer = *impl.layers_[idx]; \
|
||||||
const Region& region = layer.getRegion();
|
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", "") {
|
MATCH("/hello", "") {
|
||||||
client.receive(delay, "/hello", "", nullptr);
|
client.receive(delay, "/hello", "", nullptr);
|
||||||
} break;
|
} 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);
|
client.receive<'f'>(delay, path, value * 100.0f);
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
#define GET_FILTER_OR_BREAK(idx) \
|
|
||||||
if (idx >= region.filters.size()) \
|
|
||||||
break; \
|
|
||||||
const auto& filter = region.filters[idx];
|
|
||||||
|
|
||||||
MATCH("/region&/filter&/cutoff", "") {
|
MATCH("/region&/filter&/cutoff", "") {
|
||||||
GET_REGION_OR_BREAK(indices[0])
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
GET_FILTER_OR_BREAK(indices[1])
|
GET_FILTER_OR_BREAK(indices[1])
|
||||||
|
|
@ -1285,13 +1295,6 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
|
||||||
}
|
}
|
||||||
} break;
|
} 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", "") {
|
MATCH("/region&/eq&/gain", "") {
|
||||||
GET_REGION_OR_BREAK(indices[0])
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
GET_EQ_OR_BREAK(indices[1])
|
GET_EQ_OR_BREAK(indices[1])
|
||||||
|
|
@ -1337,9 +1340,72 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
|
||||||
}
|
}
|
||||||
} break;
|
} 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_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", "") {
|
MATCH("/num_active_voices", "") {
|
||||||
client.receive<'i'>(delay, path, impl.voiceManager_.getNumActiveVoices());
|
client.receive<'i'>(delay, path, impl.voiceManager_.getNumActiveVoices());
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,12 @@ struct Synth::Impl final: public Parser::Listener {
|
||||||
*/
|
*/
|
||||||
void finalizeSfzLoad();
|
void finalizeSfzLoad();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the regions with the opcodes received through OSC if necessary
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void updateRegions() noexcept;
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
static void collectUsedCCsFromCCMap(BitArray<config::numCCs>& usedCCs, const CCMap<T> map) noexcept
|
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 };
|
bool playheadMoved_ { false };
|
||||||
|
|
||||||
|
struct OpcodeUpdate
|
||||||
|
{
|
||||||
|
int delay;
|
||||||
|
Region* region;
|
||||||
|
Opcode opcode;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<OpcodeUpdate> regionUpdates_;
|
||||||
|
SpinMutex regionUpdatesMutex_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sfz
|
} // namespace sfz
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,7 @@ struct Voice::Impl
|
||||||
float baseVolumedB_ { 0.0 };
|
float baseVolumedB_ { 0.0 };
|
||||||
float baseGain_ { 1.0 };
|
float baseGain_ { 1.0 };
|
||||||
float baseFrequency_ { 440.0 };
|
float baseFrequency_ { 440.0 };
|
||||||
|
uint8_t pitchKeycenter_ { Default::key };
|
||||||
|
|
||||||
float floatPositionOffset_ { 0.0f };
|
float floatPositionOffset_ { 0.0f };
|
||||||
int sourcePosition_ { 0 };
|
int sourcePosition_ { 0 };
|
||||||
|
|
@ -472,6 +473,7 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
|
||||||
if (resources.stretch)
|
if (resources.stretch)
|
||||||
impl.pitchRatio_ *= resources.stretch->getRatioForFractionalKey(numberRetuned);
|
impl.pitchRatio_ *= resources.stretch->getRatioForFractionalKey(numberRetuned);
|
||||||
|
|
||||||
|
impl.pitchKeycenter_ = region.pitchKeycenter;
|
||||||
impl.baseVolumedB_ = region.getBaseVolumedB(resources.midiState, impl.triggerEvent_.number);
|
impl.baseVolumedB_ = region.getBaseVolumedB(resources.midiState, impl.triggerEvent_.number);
|
||||||
impl.baseGain_ = region.getBaseGain();
|
impl.baseGain_ = region.getBaseGain();
|
||||||
if (impl.triggerEvent_.type != TriggerEventType::CC || region.velocityOverride == VelocityOverride::previous)
|
if (impl.triggerEvent_.type != TriggerEventType::CC || region.velocityOverride == VelocityOverride::previous)
|
||||||
|
|
@ -1449,7 +1451,7 @@ void Voice::Impl::fillWithGenerator(AudioSpan<float> buffer) noexcept
|
||||||
if (!frequencies)
|
if (!frequencies)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
float keycenterFrequency = midiNoteFrequency(region_->pitchKeycenter);
|
float keycenterFrequency = midiNoteFrequency(pitchKeycenter_);
|
||||||
fill(*frequencies, pitchRatio_ * keycenterFrequency);
|
fill(*frequencies, pitchRatio_ * keycenterFrequency);
|
||||||
pitchEnvelope(*frequencies);
|
pitchEnvelope(*frequencies);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ include(Catch)
|
||||||
set(SFIZZ_TEST_SOURCES
|
set(SFIZZ_TEST_SOURCES
|
||||||
DirectRegionT.cpp
|
DirectRegionT.cpp
|
||||||
RegionValuesT.cpp
|
RegionValuesT.cpp
|
||||||
|
RegionValuesSetT.cpp
|
||||||
TestHelpers.h
|
TestHelpers.h
|
||||||
TestHelpers.cpp
|
TestHelpers.cpp
|
||||||
ParsingT.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