Skip to content

Commit

Permalink
Merge pull request #761 from paulfd/osc-region-change
Browse files Browse the repository at this point in the history
Updating regions through OSC
  • Loading branch information
paulfd authored Apr 4, 2021
2 parents d0cd21a + 78db0b7 commit 254d007
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 16 deletions.
4 changes: 2 additions & 2 deletions src/sfizz/Region.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
3 changes: 2 additions & 1 deletion src/sfizz/Region.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions src/sfizz/Synth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down Expand Up @@ -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);
Expand Down
90 changes: 78 additions & 12 deletions src/sfizz/SynthMessaging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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<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());
} break;
Expand Down
16 changes: 16 additions & 0 deletions src/sfizz/SynthPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion src/sfizz/Voice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ include(Catch)
set(SFIZZ_TEST_SOURCES
DirectRegionT.cpp
RegionValuesT.cpp
RegionValuesSetT.cpp
TestHelpers.h
TestHelpers.cpp
ParsingT.cpp
Expand Down
122 changes: 122 additions & 0 deletions tests/RegionValuesSetT.cpp
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit 254d007

Please sign in to comment.