Add setters

This commit is contained in:
Paul Fd 2024-01-18 23:22:23 +01:00 committed by Paul Ferrand
parent ad9c857513
commit 3d7bcb4968
20 changed files with 3294 additions and 4039 deletions

3
.gitmodules vendored
View file

@ -16,3 +16,6 @@
[submodule "external/st_audiofile/thirdparty/wavpack"]
path = external/st_audiofile/thirdparty/wavpack
url = https://github.com/dbry/WavPack.git
[submodule "external/invoke.hpp"]
path = external/invoke.hpp
url = https://github.com/BlackMATov/invoke.hpp.git

View file

@ -51,7 +51,7 @@ endif()
# Set macOS compatibility level
if(APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9")
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
endif()
# If using C++17, check if aligned-new has runtime support on the platform;

View file

@ -83,6 +83,11 @@ else()
sfizz_add_vendor_abseil()
endif()
# C++14 std::invoke
# add_subdirectory("external/invoke.hpp")
add_library(invoke_hpp INTERFACE)
target_include_directories(invoke_hpp INTERFACE "external/invoke.hpp/headers")
# The jsl utility library for C++
add_library(sfizz_jsl INTERFACE)
add_library(sfizz::jsl ALIAS sfizz_jsl)

View file

@ -243,6 +243,10 @@ SFIZZ_CXX_FLAGS += -I$(SFIZZ_DIR)/external/atomic_queue/include
SFIZZ_CXX_FLAGS += -I$(SFIZZ_DIR)/external/filesystem/include
# invoke.hpp dependency
SFIZZ_CXX_FLAGS += -I$(SFIZZ_DIR)/external/invoke.hpp/headers
### Abseil dependency
SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/external/abseil-cpp
@ -293,7 +297,11 @@ SFIZZ_SOURCES += \
external/abseil-cpp/absl/synchronization/blocking_counter.cc \
external/abseil-cpp/absl/synchronization/internal/create_thread_identity.cc \
external/abseil-cpp/absl/synchronization/internal/per_thread_sem.cc \
external/abseil-cpp/absl/synchronization/internal/waiter.cc \
external/abseil-cpp/absl/synchronization/internal/waiter_base.cc \
external/abseil-cpp/absl/synchronization/internal/win32_waiter.cc \
external/abseil-cpp/absl/synchronization/internal/stdcpp_waiter.cc \
external/abseil-cpp/absl/synchronization/internal/sem_waiter.cc \
external/abseil-cpp/absl/synchronization/internal/pthread_waiter.cc \
external/abseil-cpp/absl/synchronization/notification.cc \
external/abseil-cpp/absl/synchronization/mutex.cc
# absl::graphcycles_internal

2
external/abseil-cpp vendored

@ -1 +1 @@
Subproject commit c2435f8342c2d0ed8101cb43adfd605fdc52dca2
Subproject commit ad73c6dc1a253203c1c8b529cda18f2138d49df0

1
external/invoke.hpp vendored Submodule

@ -0,0 +1 @@
Subproject commit 2c1eabc2e20ab02961f95c704ff0c0818671ddd1

View file

@ -295,7 +295,7 @@ target_sources(sfizz_internal PRIVATE ${SFIZZ_HEADERS} ${SFIZZ_SOURCES} ${FAUST_
target_include_directories(sfizz_internal PUBLIC "." "sfizz")
target_link_libraries(sfizz_internal
PUBLIC absl::strings absl::span sfizz::filesystem sfizz::atomic_queue sfizz::spin_mutex sfizz::bit_array sfizz::simde sfizz::hiir sfizz::jsl
PRIVATE sfizz::parser sfizz::messaging absl::flat_hash_map Threads::Threads st_audiofile sfizz::pugixml sfizz::spline sfizz::tunings sfizz::kissfft sfizz::cephes sfizz::cpuid sfizz::threadpool sfizz::atomic)
PRIVATE sfizz::parser sfizz::messaging absl::flat_hash_map Threads::Threads st_audiofile sfizz::pugixml sfizz::spline sfizz::tunings sfizz::kissfft sfizz::cephes sfizz::cpuid sfizz::threadpool sfizz::atomic invoke_hpp)
if(SFIZZ_USE_SNDFILE)
target_compile_definitions(sfizz_internal PUBLIC "SFIZZ_USE_SNDFILE=1")
target_link_libraries(sfizz_internal PUBLIC st_audiofile)

View file

@ -70,6 +70,7 @@ FloatSpec loBipolar { -1.0f, {-1.0f, 1.0f}, kPermissiveBounds };
FloatSpec hiBipolar { 1.0f, {-1.0f, 1.0f}, kPermissiveBounds };
UInt16Spec ccNumber { 0, {0, config::numCCs}, 0 };
UInt16Spec smoothCC { 0, {0, 100}, kPermissiveUpperBound };
FloatSpec stepCC { 0.0f, {0.0f, 127.0f}, kPermissiveUpperBound };
UInt8Spec curveCC { 0, {0, 255}, 0 };
UInt8Spec sustainCC { 64, {0, 127}, 0 };
UInt8Spec sostenutoCC { 66, {0, 127}, 0 };

View file

@ -228,6 +228,7 @@ namespace Default
extern const OpcodeSpec<float> hiPolyAftertouch;
extern const OpcodeSpec<uint16_t> ccNumber;
extern const OpcodeSpec<uint8_t> curveCC;
extern const OpcodeSpec<float> stepCC;
extern const OpcodeSpec<uint16_t> smoothCC;
extern const OpcodeSpec<uint8_t> sustainCC;
extern const OpcodeSpec<uint8_t> sostenutoCC;

View file

@ -1757,10 +1757,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, OpcodeSpec<float> spec,
p.curve = opcode.read(Default::curveCC);
break;
case kOpcodeStepCcN:
{
const OpcodeSpec<float> stepCC { 0.0f, {}, kPermissiveBounds };
p.step = spec.normalizeInput(opcode.read(stepCC));
}
p.step = spec.normalizeInput(opcode.read(Default::stepCC));
break;
case kOpcodeSmoothCcN:
p.smooth = opcode.read(Default::smoothCC);

View file

@ -290,8 +290,8 @@ struct Region {
VelocityOverride velocityOverride { Default::velocityOverride }; // sw_vel
bool checkSustain { Default::checkSustain }; // sustain_sw
bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw
uint16_t sustainCC { Default::sustainCC }; // sustain_cc
uint16_t sostenutoCC { Default::sostenutoCC }; // sustain_cc
uint8_t sustainCC { Default::sustainCC }; // sustain_cc
uint8_t sostenutoCC { Default::sostenutoCC }; // sostenuto_cc
float sustainThreshold { Default::sustainThreshold }; // sustain_cc
float sostenutoThreshold { Default::sostenutoThreshold }; // sustain_cc

View file

@ -285,20 +285,19 @@ inline CXX14_CONSTEXPR bool ccModulationIsPerVoice(int cc) {
namespace literals {
inline float operator""_norm(unsigned long long int value)
{
if (value > 127)
value = 127;
return normalize7Bits(value);
return static_cast<float>(value) / 127.0f;
}
inline float operator""_norm(long double value)
{
if (value < 0)
value = 0;
if (value > 127)
value = 127;
return normalize7Bits(value);
return static_cast<float>(value) / 127.0f;
}
inline float operator""_bend(unsigned long long int value)
{
return static_cast<float>(value) / 8191.0f;
}
inline float operator""_bend(long double value)
{
return static_cast<float>(value) / 8191.0f;
}
}

View file

@ -33,6 +33,7 @@
#include "parser/Parser.h"
#include <absl/algorithm/container.h>
#include <absl/memory/memory.h>
#include <absl/strings/str_cat.h>
#include <absl/strings/str_replace.h>
#include <absl/types/optional.h>
#include <absl/types/span.h>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,708 @@
#pragma once
#include "CCMap.h"
#include "Config.h"
#include "Defaults.h"
#include "EQDescription.h"
#include "FileId.h"
#include "FilterDescription.h"
#include "FlexEGDescription.h"
#include "LFOCommon.h"
#include "LFODescription.h"
#include "Opcode.h"
#include "Range.h"
#include "SfzFilter.h"
#include "SfzHelpers.h"
#include "SynthPrivate.h"
#include "FilePool.h"
#include "Curve.h"
#include "MidiState.h"
#include "SynthConfig.h"
#include "TriggerEvent.h"
#include "SynthPrivate.h"
#include "utility/Size.h"
#include <type_traits>
#include <invoke.hpp/invoke.hpp>
namespace inv = invoke_hpp;
static uint64_t hashMessagePath(const char* path, const char* sig)
{
uint64_t h = Fnv1aBasis;
while (unsigned char c = *path++) {
if (!absl::ascii_isdigit(c))
h = hashByte(c, h);
else {
h = hashByte('&', h);
while (absl::ascii_isdigit(*path))
++path;
}
}
h = hashByte(',', h);
while (unsigned char c = *sig++)
h = hashByte(c, h);
return h;
}
template <class T>
using IntegralNotBool = std::enable_if_t<std::is_integral<T>::value && !std::is_same<T, bool>::value>;
template <class T>
using BoolOrNotIntegral = std::enable_if_t<!std::is_integral<T>::value || std::is_same<T, bool>::value>;
namespace sfz {
static constexpr unsigned maxIndices = 8;
class MessagingHelper {
public:
MessagingHelper(Client& client, int delay, const char* path, const char* sig, const sfizz_arg_t* args, Synth::Impl& impl)
: client(client)
, impl(impl)
, delay(delay)
, path(path)
, sig(sig)
, request_args(args)
{
indices.reserve(maxIndices);
}
enum class ModParam { Depth, Curve, Smooth, Step };
bool match(const char* pattern, const char* sig)
{
indices.clear();
const char* path_ = this->path;
while (const char* endp = strchr(pattern, '&')) {
if (indices.size() == maxIndices)
return false;
size_t length = endp - pattern;
if (strncmp(pattern, path_, length) != 0)
return false;
pattern += length;
path_ += length;
length = 0;
while (absl::ascii_isdigit(path_[length]))
++length;
indices.push_back(0);
if (!absl::SimpleAtoi(absl::string_view(path_, length), &indices.back()))
return false;
pattern += 1;
path_ += length;
}
return !strcmp(path_, pattern) && !strcmp(this->sig, sig);
}
// These are the reply/reply2 overloads for the (almost) concrete types, that should
// translate to actual calls to the client.receive(...) method.
void reply(const std::string& value) { client.receive<'s'>(delay, path, value.data()); }
void reply(const char* value) { client.receive<'s'>(delay, path, value); }
// void reply(float value) { client.receive<'f'>(delay, path, value); }
void reply(const float& value) { client.receive<'f'>(delay, path, value); }
void reply(absl::nullopt_t) { client.receive<'N'>(delay, path, {}); }
void reply(const bool& value)
{
if (value)
client.receive<'T'>(delay, path, {});
else
client.receive<'F'>(delay, path, {});
}
template <class T, typename = std::enable_if_t<std::is_integral<T>::value>>
void reply(const T& value)
{
if (sizeof(value) <= 4)
client.receive<'i'>(delay, path, static_cast<int>(value));
else
client.receive<'h'>(delay, path, static_cast<long int>(value));
}
template <size_t N>
void reply(const BitArray<N>& array)
{
sfizz_blob_t blob { array.data(), static_cast<uint32_t>(array.byte_size()) };
client.receive<'b'>(delay, path, &blob);
}
// Call reply but denormalizes the input if needed by the opcode spec
template <class T>
void reply(const T& value, const OpcodeSpec<T>& spec) { reply(spec.denormalizeInput(value)); }
template <class T>
BoolOrNotIntegral<T> set(T& target, const OpcodeSpec<T>& spec) { target = Opcode::read(spec, request_args[0].s); }
// sfizz specific types
void reply(const LFOWave& wave) { reply(static_cast<int>(wave)); }
void reply(const SelfMask& mode) { reply(mode == SelfMask::mask); }
void reply(const LoopMode& mode)
{
switch (mode) {
case LoopMode::no_loop: reply("no_loop"); break;
case LoopMode::loop_continuous: reply("loop_continuous"); break;
case LoopMode::loop_sustain: reply("loop_sustain"); break;
case LoopMode::one_shot: reply("one_shot"); break;
}
}
void reply(const CrossfadeCurve& curve)
{
switch (curve) {
case CrossfadeCurve::gain: reply("gain"); break;
case CrossfadeCurve::power: reply("power"); break;
}
}
void reply(const Trigger& mode)
{
switch (mode) {
case Trigger::attack: reply("attack"); break;
case Trigger::first: reply("first"); break;
case Trigger::legato: reply("legato"); break;
case Trigger::release: reply("release"); break;
case Trigger::release_key: reply("release_key"); break;
}
}
void reply(const VelocityOverride& mode)
{
switch (mode) {
case VelocityOverride::current: reply("current"); break;
case VelocityOverride::previous: reply("previous"); break;
}
}
void reply(const OffMode& mode)
{
switch (mode) {
case OffMode::fast: reply("fast"); break;
case OffMode::time: reply("time"); break;
case OffMode::normal: reply("normal"); break;
}
}
void reply(const FilterType& type)
{
switch (type) {
case FilterType::kFilterLpf1p: reply("lpf_1p"); break;
case FilterType::kFilterHpf1p: reply("hpf_1p"); break;
case FilterType::kFilterLpf2p: reply("lpf_2p"); break;
case FilterType::kFilterHpf2p: reply("hpf_2p"); break;
case FilterType::kFilterBpf2p: reply("bpf_2p"); break;
case FilterType::kFilterBrf2p: reply("brf_2p"); break;
case FilterType::kFilterBpf1p: reply("bpf_1p"); break;
case FilterType::kFilterBrf1p: reply("brf_1p"); break;
case FilterType::kFilterApf1p: reply("apf_1p"); break;
case FilterType::kFilterLpf2pSv: reply("lpf_2p_sv"); break;
case FilterType::kFilterHpf2pSv: reply("hpf_2p_sv"); break;
case FilterType::kFilterBpf2pSv: reply("bpf_2p_sv"); break;
case FilterType::kFilterBrf2pSv: reply("brf_2p_sv"); break;
case FilterType::kFilterLpf4p: reply("lpf_4p"); break;
case FilterType::kFilterHpf4p: reply("hpf_4p"); break;
case FilterType::kFilterLpf6p: reply("lpf_6p"); break;
case FilterType::kFilterHpf6p: reply("hpf_6p"); break;
case FilterType::kFilterPink: reply("pink"); break;
case FilterType::kFilterLsh: reply("lsh"); break;
case FilterType::kFilterHsh: reply("hsh"); break;
case FilterType::kFilterPeq: reply("peq"); break;
case FilterType::kFilterBpf4p: reply("bpf_4p"); break;
case FilterType::kFilterBpf6p: reply("bpf_6p"); break;
case FilterType::kFilterNone: reply("none"); break;
}
}
void reply(const EqType& type)
{
switch (type) {
case EqType::kEqNone: reply("none"); break;
case EqType::kEqPeak: reply("peak"); break;
case EqType::kEqLshelf: reply("lshelf"); break;
case EqType::kEqHshelf: reply("hshelf"); break;
}
}
void reply(const TriggerEventType& type)
{
switch (type) {
case TriggerEventType::NoteOff: reply("note_off"); break;
case TriggerEventType::NoteOn: reply("note_on"); break;
case TriggerEventType::CC: reply("cc"); break;
}
}
// reply2 for pairs (Usually only for float/int ranges)
template <class T, typename = std::enable_if_t<std::is_integral<T>::value>>
void reply2(T value1, T value2)
{
sfizz_arg_t response_args[2];
if (sizeof(value1) <= 4) {
response_args[0].i = value1;
response_args[1].i = value2;
client.receive(delay, path, "ii", response_args);
} else {
response_args[0].h = value1;
response_args[1].h = value2;
client.receive(delay, path, "hh", response_args);
}
}
void reply2(float value1, float value2)
{
sfizz_arg_t response_args[2];
response_args[0].f = value1;
response_args[1].f = value2;
client.receive(delay, path, "ff", response_args);
}
void set(std::string& target) { target = request_args[0].s; }
void set(float& target, const OpcodeSpec<float>& spec) { target = Opcode::transform(spec, request_args[0].f); }
void set(float& target) { target = request_args[0].f; }
void set(LFOWave& target, const OpcodeSpec<LFOWave>& spec) { target = Opcode::transform(spec, request_args[0].i); }
void set(bool& target, const OpcodeSpec<bool>& spec)
{
if (sig[0] == 'T') {
target = true;
} else if (sig[0] == 'F') {
target = false;
} else {
target = Opcode::read(spec, request_args[0].s);
}
}
template <class T>
IntegralNotBool<T> set(T& target, const OpcodeSpec<T>& spec)
{
if (sizeof(target) <= 4)
target = Opcode::transform(spec, request_args[0].i);
else
target = Opcode::transform(spec, request_args[0].h);
}
// Now we have some templated reply overloads that decay into "concrete" reply calls
// but add some logic (e.g. an optional can either reply the value, or send 'N' for
// null)
template <class T, class... Args>
void reply(const absl::optional<T>& value, Args... args)
{
if (!value) {
client.receive<'N'>(delay, path, {});
return;
}
reply(*value, std::forward<Args>(args)...);
}
template <class T, class... Args>
void reply(T* value, Args... args)
{
if (!value) {
client.receive<'N'>(delay, path, {});
return;
}
reply(*value, std::forward<Args>(args)...);
}
template <class T>
void reply(const std::shared_ptr<T>& value) { reply(value.get()); }
template <class T>
void reply(const UncheckedRange<T>& range) { reply2(range.getStart(), range.getEnd()); }
template <class T>
void reply(const UncheckedRange<T>& range, const OpcodeSpec<T>& startSpec, const OpcodeSpec<T>& endSpec)
{
reply2(startSpec.denormalizeInput(range.getStart()), endSpec.denormalizeInput(range.getEnd()));
}
template <class T>
IntegralNotBool<T> set(UncheckedRange<T>& range, const OpcodeSpec<T>& startSpec, const OpcodeSpec<T>& endSpec)
{
if (sizeof(range.getStart()) <= 4) {
range.setStart(Opcode::transform(startSpec, request_args[0].i));
range.setEnd(Opcode::transform(endSpec, request_args[1].i));
} else {
range.setStart(Opcode::transform(startSpec, request_args[0].h));
range.setEnd(Opcode::transform(endSpec, request_args[1].h));
}
}
template <class T>
BoolOrNotIntegral<T> set(UncheckedRange<T>& range, const OpcodeSpec<T>& startSpec, const OpcodeSpec<T>& endSpec)
{
if (sizeof(range.getStart()) <= 4) {
range.setStart(Opcode::transform(startSpec, request_args[0].f));
range.setEnd(Opcode::transform(endSpec, request_args[1].f));
} else {
range.setStart(Opcode::transform(startSpec, request_args[0].d));
range.setEnd(Opcode::transform(endSpec, request_args[1].d));
}
}
template <class T>
IntegralNotBool<T> set(UncheckedRange<T>& range)
{
if (sizeof(range.getStart()) <= 4) {
range.setStart(request_args[0].i);
range.setEnd(request_args[1].i);
} else {
range.setStart(request_args[0].h);
range.setEnd(request_args[1].h);
}
}
template <class T>
BoolOrNotIntegral<T> set(UncheckedRange<T>& range)
{
if (sizeof(range.getStart()) <= 4) {
range.setStart(request_args[0].f);
range.setEnd(request_args[1].f);
} else {
range.setStart(request_args[0].d);
range.setEnd(request_args[1].d);
}
}
template <class T>
void reply(const absl::optional<T>& value, T&& def) { reply(value.value_or(def)); }
template <class T, class...Args>
void set(absl::optional<T>& member, Args&&...args)
{
if (sig[0] == 'N') {
member.reset();
} else if (member.has_value()) {
set(*member, std::forward<Args>(args)...);
} else {
set(member.emplace(), std::forward<Args>(args)...);
}
}
template <class T, class... Args>
void reply(const ModifierCurvePair<T>& modCurve, ModParam which, Args&&... args)
{
switch (which) {
case ModParam::Curve: reply(modCurve.curve); break;
default: reply(modCurve.modifier, std::forward<Args>(args)...); break;
}
}
template <class T, class... Args>
void set(ModifierCurvePair<T>& modCurve, ModParam which, Args&&... args)
{
switch (which) {
case ModParam::Curve: modCurve.curve = request_args[0].i; break;
default: set(modCurve.modifier, args...); break;
}
}
template <class... Args>
void reply(const ModKey::Parameters& params, ModParam which, Args&&... args)
{
switch (which) {
case ModParam::Depth: break;
case ModParam::Curve: reply(params.curve); break;
case ModParam::Smooth: reply(params.smooth); break;
case ModParam::Step: reply(params.step, std::forward<Args>(args)...); break;
}
}
template <class T, class... Args>
void reply(const CCMap<T>& map, Args... args) { reply(map, true, args...); }
template <class T, class... Args>
void reply(const CCMap<T>& map, bool&& useDefault, Args... args)
{
if (useDefault)
reply(map.getWithDefault(indices.back()), args...);
else
reply(map.get(indices.back()), args...);
}
template <class T, class... Args>
void set(const CCMap<T>& map, Args... args)
{
set(map[indices.back()], args...);
}
template <class T, class... Args>
void reply(const EGDescription& eg, T EGDescription::*&& member, Args&&... args)
{
reply(eg.*member, std::forward<Args>(args)...);
}
template <class T, class... Args>
void set(EGDescription& eg, T EGDescription::*&& member, Args&&... args)
{
set(eg.*member, std::forward<Args>(args)...);
}
template <class T, class... Args>
void set(CCMap<T>& map, Args&&... args) { set(map[indices.back()], std::forward<Args>(args)...); }
// Below are all methods that will fetch various data structure elements (regions,
// egs, ...), check that they exist, and call an actual "concrete" reply
// implementation. They use pointer-to-member variables, which allow the compiler to
// dispatch to the correct logic. For example, if the pointer-to-member is
// `Region::*`, we aim at sending the value of a member of the Region
// struct. The first thing is to read the index of the region and fetch it from the
// sfizz data structure using `getRegion(...)`, and then apply the pointer-to-member
// to this particular region to send the value.
// Adding new dispatching overloads seemed simple enough to this point, although I
// haven't found a nice way to the same with pointer-to-member function.
// TODO: could do something with the c++14 compatible `invoke` maybe?
template <class T, class M, class... Args>
void set(T M::*member, Args&&... args)
{
dispatch(
static_cast<void (MessagingHelper::*)(T&, Args && ...)>(&MessagingHelper::set),
member,
std::forward<Args>(args)...);
}
// MSVC require this because it finds T M::* and T Voice::* ambiguous in the overload
// resolution of `reply`, so we "disable" the reply/set dispatching for Voice and
// TriggerEvent since they're read-only components.
template<class M>
using DispatchedType = typename std::enable_if<
!std::is_same<M, Voice>::value && !std::is_same<M, TriggerEvent>::value, void>::type;
template <class T, class M, class... Args>
DispatchedType<M> reply(T M::*member, Args&&... args)
{
dispatch(
static_cast<void (MessagingHelper::*)(const T&, Args&&...)>(&MessagingHelper::reply),
member,
std::forward<Args>(args)...);
}
template <class T, class F, class... Args>
void dispatch(F&& f, T FilterDescription::*member, Args&&... args)
{
if (auto region = getRegion())
if (auto filter = getFilter(*region))
inv::invoke(std::forward<F>(f), this, (*filter).*member, std::forward<Args>(args)...);
}
template <class T, class F, class... Args>
void dispatch(F&& f, T SynthConfig::*member, Args&&... args)
{
inv::invoke(std::forward<F>(f), this, (impl.resources_.getSynthConfig()).*member, std::forward<Args>(args)...);
}
template <class T, class F, class... Args>
void dispatch(F&& f, T LFODescription::Sub::*member, Args&&... args)
{
if (auto region = getRegion())
if (auto lfo = getLFO(*region))
if (auto sub = getLFOSub(*lfo))
inv::invoke(std::forward<F>(f), this, (*sub).*member, std::forward<Args>(args)...);
}
template <class T, class F, class... Args>
void dispatch(F&& f, T Region::*member, Args&&... args)
{
if (auto region = getRegion())
inv::invoke(std::forward<F>(f), this, (*region).*member, std::forward<Args>(args)...);
}
template <class T, class F, class... Args>
void dispatch(F&& f, T EQDescription::*member, Args&&... args)
{
if (auto region = getRegion())
if (auto eq = getEQ(*region))
inv::invoke(std::forward<F>(f), this, (*eq).*member, std::forward<Args>(args)...);
}
template <class T, class F, class... Args>
void dispatch(F&& f, T LFODescription::*member, Args&&... args)
{
if (auto region = getRegion())
if (auto lfo = getLFO(*region))
inv::invoke(std::forward<F>(f), this, (*lfo).*member, std::forward<Args>(args)...);
}
template <class T, class F, class... Args>
void dispatch(F&& f, T FlexEGPoint::*member, Args&&... args)
{
if (auto region = getRegion())
if (auto eg = getEG(*region))
if (auto point = getEGPoint(*eg))
inv::invoke(std::forward<F>(f), this, (*point).*member, std::forward<Args>(args)...);
}
// No need to dispatch for voices, they are read-only for now
template <class T, class... Args>
void reply(T TriggerEvent::*member, Args&&... args)
{
if (auto voice = getVoice())
reply((*voice).getTriggerEvent().*member, std::forward<Args>(args)...);
}
template <class T, class... Args>
void reply(T Voice::*member, Args&&... args)
{
if (auto voice = getVoice()) {
reply((*voice.*member)(std::forward<Args>(args)...)); // Voice only has callables
}
}
template <class... Args>
void reply(ModId id, ModParam param, Args&&... args)
{
if (auto region = getRegion()) {
int cc = static_cast<int>(indices.back());
switch (id) {
case ModId::FilCutoff:
case ModId::FilGain:
switch (param) {
case ModParam::Depth: reply(region->ccModDepth(cc, id, indices[1]), std::forward<Args>(args)...);
break;
default:
reply(region->ccModParameters(cc, id, indices[1]), param, std::forward<Args>(args)...);
break;
}
break;
default:
switch (param) {
case ModParam::Depth: reply(region->ccModDepth(cc, id), std::forward<Args>(args)...);
break;
default:
reply(region->ccModParameters(cc, id), param, std::forward<Args>(args)...);
break;
}
}
}
}
// Validate and fetch elements from the sfizz data structures. By default, we kind of
// assume that regions/voices will be the first index, CCs will be the last, and
// EQ/Filter/.. will be in-between.
Region* getRegion(absl::optional<unsigned> index = {})
{
const auto idx = index.value_or(indices[0]);
if (idx >= impl.layers_.size())
return {};
Layer& layer = *impl.layers_[idx];
return &layer.getRegion();
}
FilterDescription* getFilter(Region& region, absl::optional<unsigned> index = {})
{
const auto idx = index.value_or(indices[1]);
if (region.filters.size() <= idx)
return {};
return &region.filters[idx];
}
EQDescription* getEQ(Region& region, absl::optional<unsigned> index = {})
{
const auto idx = index.value_or(indices[1]);
if (region.equalizers.size() <= idx)
return {};
return &region.equalizers[idx];
}
LFODescription* getLFO(Region& region, absl::optional<unsigned> index = {})
{
const auto idx = index.value_or(indices[1]);
if (region.lfos.size() <= idx)
return {};
return &region.lfos[idx];
}
LFODescription::Sub* getLFOSub(LFODescription& lfo, absl::optional<unsigned> index = {})
{
if (indices.size() == 2) {
if (lfo.sub.empty())
return {};
else
return &lfo.sub.front();
}
const auto idx = index.value_or(indices[2]);
if (lfo.sub.size() <= idx)
return {};
return &lfo.sub[idx];
}
FlexEGDescription* getEG(Region& region, absl::optional<unsigned> index = {})
{
const auto idx = index.value_or(indices[1]);
if (region.flexEGs.size() <= idx)
return {};
return &region.flexEGs[idx];
}
FlexEGPoint* getEGPoint(FlexEGDescription& desc, absl::optional<unsigned> index = {})
{
const auto idx = index.value_or(indices[2]) + 1;
if (desc.points.size() <= idx)
return {};
return &desc.points[idx];
}
Voice* getVoice(absl::optional<unsigned> index = {})
{
const auto idx = index.value_or(indices[0]);
if (static_cast<int>(idx) >= impl.numVoices_)
return {};
auto& voice = impl.voiceManager_[idx];
if (voice.isFree())
return {};
return &voice;
}
// Helpers to get and check the values of the indices
template <class T = unsigned>
absl::optional<T> index(int i)
{
if (i <= ssize(indices))
return static_cast<T>(indices[i]);
return absl::nullopt;
}
absl::optional<int> sindex(int i) { return index<int>(i); }
absl::optional<int> checkCC(int i = 0)
{
auto cc = index(i);
return cc <= config::numCCs ? cc : absl::nullopt;
}
absl::optional<int> checkNote(int i = 0)
{
auto note = sindex(i);
return note <= 127 ? note : absl::nullopt;
}
private:
Client& client;
std::vector<unsigned> indices;
Synth::Impl& impl;
int delay;
const char* path;
const char* sig;
const sfizz_arg_t* request_args;
};
}

View file

@ -13,6 +13,7 @@
#include "SIMDHelpers.h"
#include "utility/Debug.h"
#include <absl/container/flat_hash_map.h>
#include <absl/strings/str_cat.h>
#include <absl/strings/string_view.h>
#include <vector>
#include <algorithm>

View file

@ -10,6 +10,7 @@
#include <iostream>
#include "catch2/catch.hpp"
#include "absl/strings/string_view.h"
#include "absl/strings/str_cat.h"
using namespace Catch::literals;
struct ParsingMocker: sfz::Parser::Listener

View file

@ -4,119 +4,509 @@
// 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 "SynthDiscussion.h"
#include "SfzHelpers.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;
using namespace sfz::literals;
using namespace std::literals;
TEST_CASE("[Set values] Pitch keycenter")
using OSC = OSCValueLess;
TEST_CASE("Set values", "[parsing][OSC]")
{
Synth synth;
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
AudioBuffer<float> buffer { 2, 256 };
SynthDiscussion d;
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);
SECTION("Pitch keycenter") {
d.load(R"( <region> sample=*sine pitch_keycenter=48 )");
REQUIRE( d.read<int32_t>("/region0/pitch_keycenter") == 48);
REQUIRE( d.sendAndRead("/region0/pitch_keycenter", 60) == 60);
}
// Update value
sfizz_arg_t args;
args.i = 60;
synth.dispatchMessage(client, 1, "/region0/pitch_keycenter", "i", &args);
synth.renderBlock(buffer);
SECTION("LFO Wave") {
d.load(R"( <region> sample=*sine lfo1_wave=5 lfo1_wave2=4 )");
REQUIRE( d.read<int32_t>("/region0/lfo0/wave") == 5);
REQUIRE( d.read<int32_t>("/region0/lfo0/wave1") == 4);
REQUIRE( d.sendAndRead("/region0/lfo0/wave", 3) == 3);
REQUIRE( d.sendAndRead("/region0/lfo0/wave1", 2) == 2);
}
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);
SECTION("Loop mode") {
d.load(R"( <region> sample=looped_flute.wav )");
REQUIRE( d.read<std::string>("/region0/loop_mode") == "loop_continuous");
REQUIRE( d.sendAndRead<std::string>("/region0/loop_mode", "one_shot") == "one_shot");
}
SECTION("Sample quality") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.read<int32_t>("/sample_quality") == 2);
REQUIRE( d.read<int32_t>("/oscillator_quality") == 1);
REQUIRE( d.read<int32_t>("/freewheeling_sample_quality") == 10);
REQUIRE( d.read<int32_t>("/freewheeling_oscillator_quality") == 3);
REQUIRE( d.sendAndRead("/sample_quality", 3) == 3);
REQUIRE( d.sendAndRead("/oscillator_quality", 2) == 2);
REQUIRE( d.sendAndRead("/freewheeling_sample_quality", 6) == 6);
REQUIRE( d.sendAndRead("/freewheeling_oscillator_quality", 2) == 2);
}
SECTION("Sustain cancels release") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.read<OSC>("/sustain_cancels_release") == OSC::False );
d.send("/sustain_cancels_release", true);
REQUIRE( d.read<OSC>("/sustain_cancels_release") == OSC::True );
d.send("/sustain_cancels_release", false);
REQUIRE( d.read<OSC>("/sustain_cancels_release") == OSC::False );
d.send("/sustain_cancels_release", "on"s);
REQUIRE( d.read<OSC>("/sustain_cancels_release") == OSC::True );
}
SECTION("Delay") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/delay", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/delay_random", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/delay_cc1", 10.0f) == 10.0f);
}
SECTION("Offset") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int64_t>("/region0/offset", 10) == 10);
REQUIRE( d.sendAndRead<int64_t>("/region0/offset_random", 10) == 10);
REQUIRE( d.sendAndRead<int64_t>("/region0/offset_cc1", 10) == 10);
}
SECTION("End") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int64_t>("/region0/end", 10) == 10);
REQUIRE( d.sendAndRead<int64_t>("/region0/end_cc1", 10) == 10);
}
SECTION("Count") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/count", 3) == 3);
}
SECTION("Loop range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<int64_t> v {13, 2000};
REQUIRE( d.sendAndReadAll<int64_t>("/region0/loop_range", v) == v);
REQUIRE( d.sendAndRead<int64_t>("/region0/loop_start_cc1", 10) == 10);
REQUIRE( d.sendAndRead<int64_t>("/region0/loop_end_cc1", 1000) == 1000);
}
SECTION("Loop count") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/loop_count", 3) == 3);
d.send("/region0/loop_count", nullptr);
REQUIRE( d.read<OSC>("/region0/loop_count") == OSC::None);
}
SECTION("Output") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/output", 3) == 3);
}
SECTION("Off by") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.read<OSC>("/region0/off_by") == OSC::None);
REQUIRE( d.sendAndRead<int64_t>("/region0/off_by", 2) == 2);
d.send("/region0/off_by", nullptr);
REQUIRE( d.read<OSC>("/region0/off_by") == OSC::None);
}
SECTION("Off mode") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<std::string>("/region0/off_mode", "time") =="time");
REQUIRE( d.sendAndRead<std::string>("/region0/off_mode", "fast") =="fast");
}
SECTION("Key range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<int32_t> v {5, 67};
REQUIRE( d.sendAndReadAll<int32_t>("/region0/key_range", v) == v);
}
SECTION("Off time") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<float>("/region0/off_time", 0.1f) == 0.1f);
}
SECTION("Velocity range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_norm, 67_norm};
REQUIRE( d.sendAndReadAll<float>("/region0/vel_range", v) == v);
}
SECTION("Bend range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_bend, 67_bend};
REQUIRE( d.sendAndReadAll<float>("/region0/bend_range", v) == v);
}
SECTION("Program range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<int32_t> v {2, 10};
REQUIRE( d.sendAndReadAll<int32_t>("/region0/program_range", v) == v);
}
SECTION("CC range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_norm, 67_norm};
REQUIRE( d.sendAndReadAll<float>("/region0/cc_range2", v) == v);
}
SECTION("Last keyswitch") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/sw_last", 24) == 24);
std::vector<int32_t> v {10, 15};
REQUIRE( d.sendAndReadAll<int32_t>("/region0/sw_last", v) == v);
}
SECTION("Keyswitch label") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<std::string>("/region0/sw_label", "hello") == "hello");
}
SECTION("Keyswitch up") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/sw_up", 12) == 12);
d.send("/region0/sw_up", "c4"s);
REQUIRE( d.read<int32_t>("/region0/sw_up") == 60);
}
SECTION("Keyswitch down") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/sw_down", 12) == 12);
d.send("/region0/sw_down", "c4"s);
REQUIRE( d.read<int32_t>("/region0/sw_down") == 60);
}
SECTION("Keyswitch down") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/sw_previous", 12) == 12);
d.send("/region0/sw_previous", "c4"s);
REQUIRE( d.read<int32_t>("/region0/sw_previous") == 60);
}
SECTION("Velocity override") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<std::string>("/region0/sw_vel", "previous") == "previous");
}
SECTION("Channel aftertouch range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_norm, 67_norm};
REQUIRE( d.sendAndReadAll<float>("/region0/chanaft_range", v) == v);
}
SECTION("Poly aftertouch range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_norm, 67_norm};
REQUIRE( d.sendAndReadAll<float>("/region0/polyaft_range", v) == v);
}
SECTION("BPM range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5, 67};
REQUIRE( d.sendAndReadAll<float>("/region0/bpm_range", v) == v);
}
SECTION("Rand range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_norm, 67_norm};
REQUIRE( d.sendAndReadAll<float>("/region0/rand_range", v) == v);
}
SECTION("Sequences") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/seq_length", 2) == 2);
REQUIRE( d.sendAndRead<int32_t>("/region0/seq_position", 2) == 2);
}
SECTION("Trigger") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<std::string>("/region0/trigger", "release") == "release");
}
SECTION("Start CC range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_norm, 67_norm};
REQUIRE( d.sendAndReadAll<float>("/region0/start_cc_range2", v) == v);
}
SECTION("Volume etc") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/volume", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/pan", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/width", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/position", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/amplitude", 10.0f) == 10.0f);
}
SECTION("Amp key something") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/amp_keycenter", 48) == 48);
REQUIRE( d.sendAndRead("/region0/amp_keytrack", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/amp_veltrack", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/amp_veltrack_cc3", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/amp_veltrack_curvecc3", 2) == 2);
}
SECTION("Amp random") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/amp_random", 10.0f) == 10.0f);
}
SECTION("Crossfade key range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<int32_t> v {5, 67};
REQUIRE( d.sendAndReadAll<int32_t>("/region0/xfin_key_range", v) == v);
REQUIRE( d.sendAndReadAll<int32_t>("/region0/xfout_key_range", v) == v);
}
SECTION("Other crossfade range") {
d.load(R"( <region> sample=kick.wav )");
std::vector<float> v {5_norm, 67_norm};
REQUIRE( d.sendAndReadAll<float>("/region0/xfin_vel_range", v) == v);
REQUIRE( d.sendAndReadAll<float>("/region0/xfout_vel_range", v) == v);
REQUIRE( d.sendAndReadAll<float>("/region0/xfin_cc_range3", v) == v);
REQUIRE( d.sendAndReadAll<float>("/region0/xfout_cc_range3", v) == v);
}
SECTION("Crossfade curves") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<std::string>("/region0/xf_keycurve", "power") == "power");
REQUIRE( d.sendAndRead<std::string>("/region0/xf_velcurve", "power") == "power");
REQUIRE( d.sendAndRead<std::string>("/region0/xf_cccurve", "power") == "power");
}
SECTION("Global amps and volumes curves") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<float>("/region0/global_volume", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/master_volume", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/group_volume", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/global_amplitude", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/master_amplitude", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/group_amplitude", 10.0f) == 10.0f);
}
SECTION("Pitch and transpose") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/pitch", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/transpose", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/pitch_random", 10.0f) == 10.0f);
}
SECTION("Pitch key something") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/pitch_keycenter", 48) == 48);
REQUIRE( d.sendAndRead("/region0/pitch_keytrack", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/pitch_veltrack", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/pitch_veltrack_cc3", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/pitch_veltrack_curvecc3", 2) == 2);
}
SECTION("Bends") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/bend_up", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/bend_down", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/bend_step", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead("/region0/bend_smooth", 10) == 10);
}
SECTION("Ampeg") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/ampeg_attack", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_delay", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_decay", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_hold", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_release", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_start", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_sustain", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_depth", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_attack_cc1", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_decay_cc2", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_delay_cc3", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_hold_cc4", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_release_cc5", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_sustain_cc6", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_start_cc7", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_attack_curvecc1", 2) == 2);
REQUIRE( d.sendAndRead("/region0/ampeg_decay_curvecc2", 2) == 2);
REQUIRE( d.sendAndRead("/region0/ampeg_delay_curvecc3", 2) == 2);
REQUIRE( d.sendAndRead("/region0/ampeg_hold_curvecc4", 2) == 2);
REQUIRE( d.sendAndRead("/region0/ampeg_release_curvecc5", 2) == 2);
REQUIRE( d.sendAndRead("/region0/ampeg_sustain_curvecc6", 2) == 2);
REQUIRE( d.sendAndRead("/region0/ampeg_start_curvecc7", 2) == 2);
REQUIRE( d.sendAndRead("/region0/ampeg_vel2attack", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_vel2delay", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_vel2decay", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_vel2hold", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_vel2release", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_vel2sustain", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/ampeg_vel2depth", 1.0f) == 1.0f);
REQUIRE( d.read<OSC>("/region0/ampeg_dynamic") == OSC::False);
d.send("/region0/ampeg_dynamic", true);
REQUIRE( d.read<OSC>("/region0/ampeg_dynamic") == OSC::True);
d.send("/region0/ampeg_dynamic", "off"s);
REQUIRE( d.read<OSC>("/region0/ampeg_dynamic") == OSC::False);
}
SECTION("Fileg") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/fileg_attack", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_delay", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/fileg_decay", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/fileg_hold", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/fileg_release", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/fileg_start", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/fileg_sustain", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/fileg_depth", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/fileg_attack_cc1", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_decay_cc2", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_delay_cc3", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_hold_cc4", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_release_cc5", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_sustain_cc6", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_start_cc7", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/fileg_attack_curvecc1", 2) == 2);
REQUIRE( d.sendAndRead("/region0/fileg_decay_curvecc2", 2) == 2);
REQUIRE( d.sendAndRead("/region0/fileg_delay_curvecc3", 2) == 2);
REQUIRE( d.sendAndRead("/region0/fileg_hold_curvecc4", 2) == 2);
REQUIRE( d.sendAndRead("/region0/fileg_release_curvecc5", 2) == 2);
REQUIRE( d.sendAndRead("/region0/fileg_sustain_curvecc6", 2) == 2);
REQUIRE( d.sendAndRead("/region0/fileg_start_curvecc7", 2) == 2);
REQUIRE( d.read<OSC>("/region0/fileg_dynamic") == OSC::False);
d.send("/region0/fileg_dynamic", true);
REQUIRE( d.read<OSC>("/region0/fileg_dynamic") == OSC::True);
d.send("/region0/fileg_dynamic", "off"s);
REQUIRE( d.read<OSC>("/region0/fileg_dynamic") == OSC::False);
}
SECTION("PitchEG") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead("/region0/pitcheg_attack", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_delay", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_decay", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_hold", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_release", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_start", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_sustain", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_depth", 2.0f) == 2.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_attack_cc1", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_decay_cc2", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_delay_cc3", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_hold_cc4", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_release_cc5", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_sustain_cc6", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_start_cc7", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead("/region0/pitcheg_attack_curvecc1", 2) == 2);
REQUIRE( d.sendAndRead("/region0/pitcheg_decay_curvecc2", 2) == 2);
REQUIRE( d.sendAndRead("/region0/pitcheg_delay_curvecc3", 2) == 2);
REQUIRE( d.sendAndRead("/region0/pitcheg_hold_curvecc4", 2) == 2);
REQUIRE( d.sendAndRead("/region0/pitcheg_release_curvecc5", 2) == 2);
REQUIRE( d.sendAndRead("/region0/pitcheg_sustain_curvecc6", 2) == 2);
REQUIRE( d.sendAndRead("/region0/pitcheg_start_curvecc7", 2) == 2);
REQUIRE( d.read<OSC>("/region0/pitcheg_dynamic") == OSC::False);
d.send("/region0/pitcheg_dynamic", true);
REQUIRE( d.read<OSC>("/region0/pitcheg_dynamic") == OSC::True);
d.send("/region0/pitcheg_dynamic", "off"s);
REQUIRE( d.read<OSC>("/region0/pitcheg_dynamic") == OSC::False);
}
SECTION("Note polyphony") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<int32_t>("/region0/note_polyphony", 3) == 3);
}
SECTION("RT dead") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.read<OSC>("/region0/rt_dead") == OSC::False );
d.send("/region0/rt_dead", true);
REQUIRE( d.read<OSC>("/region0/rt_dead") == OSC::True );
d.send("/region0/rt_dead", false);
REQUIRE( d.read<OSC>("/region0/rt_dead") == OSC::False );
d.send("/region0/rt_dead", "on"s);
REQUIRE( d.read<OSC>("/region0/rt_dead") == OSC::True );
}
SECTION("Sustain/sostenuto") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.read<OSC>("/region0/sustain_sw") == OSC::True );
d.send("/region0/sustain_sw", false);
REQUIRE( d.read<OSC>("/region0/sustain_sw") == OSC::False );
d.send("/region0/sustain_sw", true);
REQUIRE( d.read<OSC>("/region0/sustain_sw") == OSC::True );
d.send("/region0/sustain_sw", "off"s);
REQUIRE( d.read<OSC>("/region0/sustain_sw") == OSC::False );
REQUIRE( d.read<OSC>("/region0/sostenuto_sw") == OSC::True );
d.send("/region0/sostenuto_sw", false);
REQUIRE( d.read<OSC>("/region0/sostenuto_sw") == OSC::False );
d.send("/region0/sostenuto_sw", true);
REQUIRE( d.read<OSC>("/region0/sostenuto_sw") == OSC::True );
d.send("/region0/sostenuto_sw", "off"s);
REQUIRE( d.read<OSC>("/region0/sostenuto_sw") == OSC::False );
REQUIRE( d.sendAndRead<int32_t>("/region0/sustain_cc", 23) == 23);
REQUIRE( d.sendAndRead<int32_t>("/region0/sostenuto_cc", 23) == 23);
REQUIRE( d.sendAndRead<float>("/region0/sustain_lo", 0.1f) == 0.1f);
REQUIRE( d.sendAndRead<float>("/region0/sostenuto_lo", 0.1f) == 0.1f);
}
SECTION("Note selfmask") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.read<OSC>("/region0/note_selfmask") == OSC::True );
d.send("/region0/note_selfmask", "off"s);
REQUIRE( d.read<OSC>("/region0/note_selfmask") == OSC::False );
d.send("/region0/note_selfmask", "mask"s);
REQUIRE( d.read<OSC>("/region0/note_selfmask") == OSC::True );
}
SECTION("Oscillator stuff") {
d.load(R"( <region> sample=kick.wav )");
REQUIRE( d.sendAndRead<float>("/region0/oscillator_phase", 0.1f) == 0.1f);
REQUIRE( d.sendAndRead<int32_t>("/region0/oscillator_quality", 2) == 2);
REQUIRE( d.sendAndRead<int32_t>("/region0/oscillator_mode", 1) == 1);
REQUIRE( d.sendAndRead<int32_t>("/region0/oscillator_multi", 5) == 5);
REQUIRE( d.sendAndRead<float>("/region0/oscillator_detune", 0.2f) == 0.2f);
REQUIRE( d.sendAndRead<float>("/region0/oscillator_mod_depth", 0.2f) == 0.2f);
}
SECTION("Effect") {
d.load(R"( <region> sample=kick.wav effect1=10)");
REQUIRE( d.sendAndRead<float>("/region0/effect1", 1.0f) == 1.0f);
}
SECTION("Filters") {
d.load(R"( <region> sample=kick.wav)");
REQUIRE(d.read<int32_t>("/region0/add_filter") == 0);
REQUIRE( d.sendAndRead<float>("/region0/filter0/cutoff", 100.0f) == 100.0f);
REQUIRE( d.sendAndRead<float>("/region0/filter0/resonance", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/filter0/gain", 4.0f) == 4.0f);
REQUIRE( d.sendAndRead<int32_t>("/region0/filter0/keycenter", 42) == 42);
REQUIRE( d.sendAndRead<float>("/region0/filter0/keytrack", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/filter0/veltrack", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/filter0/veltrack_cc1", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<int32_t>("/region0/filter0/veltrack_curvecc2", 3) == 3);
REQUIRE( d.sendAndRead<std::string>("/region0/filter0/type", "lpf_2p") == "lpf_2p");
}
SECTION("EQs") {
d.load(R"( <region> sample=kick.wav)");
REQUIRE(d.read<int32_t>("/region0/add_eq") == 0);
REQUIRE( d.sendAndRead<float>("/region0/eq0/gain", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/eq0/bandwidth", 100.0f) == 100.0f);
REQUIRE( d.sendAndRead<float>("/region0/eq0/frequency", 500.0f) == 500.0f);
REQUIRE( d.sendAndRead<float>("/region0/eq0/vel2freq", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<float>("/region0/eq0/vel2gain", 10.0f) == 10.0f);
REQUIRE( d.sendAndRead<std::string>("/region0/eq0/type", "hshelf") == "hshelf");
}
SECTION("EGs") {
d.load(R"( <region> sample=kick.wav)");
REQUIRE(d.read<int32_t>("/region0/add_eg") == 0);
REQUIRE(d.read<int32_t>("/region0/eg0/add_point") == 0);
REQUIRE( d.sendAndRead<float>("/region0/eg0/point0/time", 1.0f) == 1.0f);
REQUIRE( d.sendAndRead<float>("/region0/eg0/point0/level", 0.5f) == 0.5f);
}
}

File diff suppressed because it is too large Load diff

187
tests/SynthDiscussion.h Normal file
View file

@ -0,0 +1,187 @@
// 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
#pragma once
#include <cstddef>
#include <string>
#include <absl/strings/str_cat.h>
#include <absl/types/variant.h>
#include "absl/strings/string_view.h"
#include "sfizz/Synth.h"
#include "sfizz_message.h"
#include "catch2/catch.hpp"
#include "utility/Size.h"
using namespace sfz;
constexpr int maxArgs = 8;
enum class OSCValueLess { True, False, None };
using OSCVariant = absl::variant<int32_t, int64_t, float, double, std::string, OSCValueLess>;
// template <class ... Args>
// struct vecToTuple
// {
// template <unsigned I>
// static void rewrap(std::tuple<Args...> &tup, const std::vector<OSCVariant>& variants)
// {
// std::get<I-1>(tup) = std::get<decltype(std::get<I-1>(tup))>(variants[I-1]);
// rewrap<I-1>(tup, variants);
// }
// template <>
// static void rewrap<0>(std::tuple<Args...> &, const std::vector<OSCVariant>&) { }
// };
// template<int MaxArgs = 8>
struct SynthDiscussion
{
SynthDiscussion()
{
client.setReceiveCallback(&SynthDiscussion::receiver);
}
static void receiver(void* data, int, const char* path, const char* sig, const sfizz_arg_t* args)
{
SynthDiscussion* self = (SynthDiscussion*)data;
self->recv_path = path;
self->recv_sig = sig;
self->recv_args.clear();
for (int i = 0, n = self->recv_sig.size(); i < n; ++i) {
switch (self->recv_sig[i]) {
case 'i': self->recv_args.emplace_back(args[i].i); break;
case 'f': self->recv_args.emplace_back(args[i].f); break;
case 'd': self->recv_args.emplace_back(args[i].d); break;
case 'h': self->recv_args.emplace_back(args[i].h); break;
case 's': self->recv_args.emplace_back(args[i].s); break;
case 'T': self->recv_args.emplace_back(OSCValueLess::True); break;
case 'F': self->recv_args.emplace_back(OSCValueLess::False); break;
case 'N': self->recv_args.emplace_back(OSCValueLess::None); break;
default:
ASSERTFALSE;
}
}
}
void load(absl::string_view sfz)
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/discussion.sfz", sfz);
}
void send(absl::string_view path, int32_t value)
{
sent_args[0].i = value;
synth.dispatchMessage(client, 0, path.data(), "i", sent_args);
}
void send(absl::string_view path, int64_t value)
{
sent_args[0].h = value;
synth.dispatchMessage(client, 0, path.data(), "h", sent_args);
}
void send(absl::string_view path, float value)
{
sent_args[0].f = value;
synth.dispatchMessage(client, 0, path.data(), "f", sent_args);
}
void send(absl::string_view path, const std::string& value)
{
sent_args[0].s = value.c_str();
synth.dispatchMessage(client, 0, path.data(), "s", sent_args);
}
void send(absl::string_view path, std::nullptr_t)
{
synth.dispatchMessage(client, 0, path.data(), "N", nullptr);
}
void send(absl::string_view path, bool value)
{
synth.dispatchMessage(client, 0, path.data(), value ? "T" : "F", nullptr);
}
template<class T> T read(absl::string_view path)
{
synth.dispatchMessage(client, 0, path.data(), "", nullptr);
return absl::get<T>(recv_args[0]);
}
bool replied(absl::string_view path)
{
recv_path = "";
synth.dispatchMessage(client, 0, path.data(), "", nullptr);
return !recv_path.empty();
}
template<class T> std::vector<T> readAll(absl::string_view path)
{
synth.dispatchMessage(client, 0, path.data(), "", nullptr);
std::vector<T> returned;
for (const auto& arg: recv_args)
returned.push_back(absl::get<T>(arg));
return returned;
}
void sendAll(absl::string_view path, const std::vector<float>& value)
{
std::string signature(value.size(), 'f');
for (int i = 0; i < ssize(value) && i < maxArgs; ++i)
sent_args[i].f = value[i];
synth.dispatchMessage(client, 0, path.data(), signature.c_str(), sent_args);
}
void sendAll(absl::string_view path, const std::vector<int32_t>& value)
{
std::string signature(value.size(), 'i');
for (int i = 0; i < ssize(value) && i < maxArgs; ++i)
sent_args[i].i = value[i];
synth.dispatchMessage(client, 0, path.data(), signature.c_str(), sent_args);
}
void sendAll(absl::string_view path, const std::vector<int64_t>& value)
{
std::string signature(value.size(), 'h');
for (int i = 0; i < ssize(value) && i < maxArgs; ++i)
sent_args[i].h = value[i];
synth.dispatchMessage(client, 0, path.data(), signature.c_str(), sent_args);
}
template<class T>
T sendAndRead(absl::string_view path, T value)
{
send(path, value);
return read<T>(path);
}
template<class T>
std::vector<T> sendAndReadAll(absl::string_view path, const std::vector<T>& value)
{
sendAll(path, value);
return readAll<T>(path);
}
std::string formatLast() const
{
std::string newMessage = absl::StrCat(recv_path, ",", recv_sig, " : { ");
for (unsigned i = 0, n = recv_sig.size(); i < n; ++i) {
absl::visit([&](auto&& arg){ absl::StrAppend(&newMessage, arg); }, recv_args[i]);
if (i == (n - 1))
absl::StrAppend(&newMessage, " }");
else
absl::StrAppend(&newMessage, ", ");
}
return newMessage;
}
Synth synth;
sfizz_arg_t sent_args[maxArgs];
std::vector<OSCVariant> recv_args;
std::string recv_path;
std::string recv_sig;
AudioBuffer<float> buffer { 2, 256 };
Client client { this };
};