From c118ad6cbe0db6c7e96af25b18bdeb571abc40ad Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Tue, 23 Feb 2021 01:39:25 +0100 Subject: [PATCH] Add sw_vel Also add some introspection for voices using OSC and tests --- src/sfizz/MidiState.cpp | 7 +++ src/sfizz/MidiState.h | 12 ++++ src/sfizz/Synth.cpp | 11 ++-- src/sfizz/SynthMessaging.cpp | 39 +++++++++++++ tests/RegionTriggersT.cpp | 109 +++++++++++++++++++++++++++++++++-- tests/RegionValuesT.cpp | 35 ----------- tests/TestHelpers.cpp | 34 +++++++++++ tests/TestHelpers.h | 6 ++ 8 files changed, 210 insertions(+), 43 deletions(-) diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 1f6cc4b5..52c7f188 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -21,6 +21,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex if (noteNumber >= 0 && noteNumber < 128) { lastNoteVelocities[noteNumber] = velocity; noteOnTimes[noteNumber] = internalClock + static_cast(delay); + lastNotePlayed = noteNumber; activeNotes++; } @@ -105,6 +106,11 @@ float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept return lastNoteVelocities[noteNumber]; } +float sfz::MidiState::getLastVelocity() const noexcept +{ + return lastNoteVelocities[lastNotePlayed]; +} + void sfz::MidiState::insertEventInVector(EventVector& events, int delay, float value) { const auto insertionPoint = absl::c_upper_bound(events, delay, MidiEventDelayComparator {}); @@ -168,6 +174,7 @@ void sfz::MidiState::reset() noexcept activeNotes = 0; internalClock = 0; + lastNotePlayed = 0; absl::c_fill(noteOnTimes, 0); absl::c_fill(noteOffTimes, 0); } diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index bbdf58e1..03306d77 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -84,6 +84,13 @@ public: */ float getNoteVelocity(int noteNumber) const noexcept; + /** + * @brief Get the velocity of the last note played + * + * @return float + */ + float getLastVelocity() const noexcept; + /** * @brief Register a pitch bend event * @@ -184,6 +191,11 @@ private: */ MidiNoteArray lastNoteVelocities; + /** + * @brief Last note played + */ + int lastNotePlayed { 0 }; + /** * @brief Current known values for the CCs. * diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b359601f..2a0b9f43 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -988,8 +988,8 @@ void Synth::noteOn(int delay, int noteNumber, uint8_t velocity) noexcept Impl& impl = *impl_; const auto normalizedVelocity = normalizeVelocity(velocity); ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration }; - impl.resources_.midiState.noteOnEvent(delay, noteNumber, normalizedVelocity); impl.noteOnDispatch(delay, noteNumber, normalizedVelocity); + impl.resources_.midiState.noteOnEvent(delay, noteNumber, normalizedVelocity); } void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept @@ -1000,7 +1000,6 @@ void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept Impl& impl = *impl_; const auto normalizedVelocity = normalizeVelocity(velocity); ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration }; - impl.resources_.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); // FIXME: Some keyboards (e.g. Casio PX5S) can send a real note-off velocity. In this case, do we have a // way in sfz to specify that a release trigger should NOT use the note-on velocity? @@ -1011,6 +1010,7 @@ void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept voice.registerNoteOff(delay, noteNumber, replacedVelocity); impl.noteOffDispatch(delay, noteNumber, replacedVelocity); + impl.resources_.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); } void Synth::Impl::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept @@ -1051,7 +1051,6 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex { const auto randValue = randNoteDistribution_(Random::randomGenerator); SisterVoiceRingBuilder ring; - const TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity }; if (!lastKeyswitchLists_[noteNumber].empty()) { if (currentSwitch_ && *currentSwitch_ != noteNumber) { @@ -1079,6 +1078,10 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex } } + TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity }; + if (region->velocityOverride == VelocityOverride::previous) + triggerEvent.value = resources_.midiState.getLastVelocity(); + startVoice(region, delay, triggerEvent, ring); } } @@ -1140,7 +1143,6 @@ void Synth::Impl::performHdcc(int delay, int ccNumber, float normValue, bool asM ASSERT(ccNumber >= 0); ScopedTiming logger { dispatchDuration_, ScopedTiming::Operation::addToDuration }; - resources_.midiState.ccEvent(delay, ccNumber, normValue); changedCCsThisCycle_.set(ccNumber); @@ -1162,6 +1164,7 @@ void Synth::Impl::performHdcc(int delay, int ccNumber, float normValue, bool asM voice.registerCC(delay, ccNumber, normValue); ccDispatch(delay, ccNumber, normValue); + resources_.midiState.ccEvent(delay, ccNumber, normValue); } void Synth::Impl::setDefaultHdcc(int ccNumber, float value) diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 123fac10..1faa7786 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -1202,6 +1202,45 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co #undef GET_EQ_OR_BREAK #undef GET_REGION_OR_BREAK + + MATCH("/num_active_voices", "") { + client.receive<'i'>(delay, path, impl.voiceManager_.getNumActiveVoices()); + } break; + + #define GET_VOICE_OR_BREAK(idx) \ + if (static_cast(idx) >= impl.numVoices_) \ + break; \ + const auto& voice = impl.voiceManager_[idx]; \ + if (voice.isFree()) \ + break; + + MATCH("/voice&/trigger_value", "") { + GET_VOICE_OR_BREAK(indices[0]) + client.receive<'f'>(delay, path, voice.getTriggerEvent().value); + } break; + + MATCH("/voice&/trigger_number", "") { + GET_VOICE_OR_BREAK(indices[0]) + client.receive<'i'>(delay, path, voice.getTriggerEvent().number); + } break; + + MATCH("/voice&/trigger_type", "") { + GET_VOICE_OR_BREAK(indices[0]) + const auto& event = voice.getTriggerEvent(); + switch (event.type) { + case TriggerEventType::CC: + client.receive<'s'>(delay, path, "cc"); + break; + case TriggerEventType::NoteOn: + client.receive<'s'>(delay, path, "note_on"); + break; + case TriggerEventType::NoteOff: + client.receive<'s'>(delay, path, "note_on"); + break; + } + + } break; + #undef MATCH // TODO... } diff --git a/tests/RegionTriggersT.cpp b/tests/RegionTriggersT.cpp index 0226f263..fa8b5f59 100644 --- a/tests/RegionTriggersT.cpp +++ b/tests/RegionTriggersT.cpp @@ -4,16 +4,19 @@ // 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 "sfizz/Region.h" #include "sfizz/SfzHelpers.h" #include "catch2/catch.hpp" using namespace Catch::literals; using namespace sfz::literals; +using namespace sfz; TEST_CASE("Basic triggers", "Region triggers") { - sfz::MidiState midiState; - sfz::Region region { 0, midiState }; + MidiState midiState; + Region region { 0, midiState }; region.parseOpcode({ "sample", "*sine" }); SECTION("key") @@ -163,8 +166,8 @@ TEST_CASE("Basic triggers", "Region triggers") TEST_CASE("Legato triggers", "Region triggers") { - sfz::MidiState midiState; - sfz::Region region { 0, midiState }; + MidiState midiState; + Region region { 0, midiState }; region.parseOpcode({ "sample", "*sine" }); SECTION("First note playing") { @@ -200,3 +203,101 @@ TEST_CASE("Legato triggers", "Region triggers") REQUIRE(!region.registerNoteOn(42, 64_norm, 0.5f)); } } + +TEST_CASE("[Triggers] sw_vel, basic") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"( + key=60 sample=kick.wav + key=62 sw_previous=60 sw_vel=previous sample=snare.wav + )"); + synth.noteOn(0, 60, 127); + synth.noteOn(10, 62, 10); + synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr); + synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr); + std::vector expected { + "/num_active_voices,i : { 2 }", + "/voice0/trigger_value,f : { 1 }", + "/voice1/trigger_value,f : { 1 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Triggers] sw_vel, without sw_previous") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"( + key=60 sample=kick.wav + key=62 sw_vel=previous sample=snare.wav + )"); + synth.noteOn(0, 60, 127); + synth.noteOn(10, 62, 10); + synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr); + synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr); + std::vector expected { + "/num_active_voices,i : { 2 }", + "/voice0/trigger_value,f : { 1 }", + "/voice1/trigger_value,f : { 1 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Triggers] sw_vel, with a note in between") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"( + key=60 sample=kick.wav + key=62 sw_vel=previous sample=snare.wav + key=64 sample=closedhat.wav + )"); + synth.noteOn(0, 60, 127); + synth.noteOn(5, 64, 63); + synth.noteOn(10, 62, 10); + synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr); + synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr); + synth.dispatchMessage(client, 0, "/voice2/trigger_value", "", nullptr); + std::vector expected { + "/num_active_voices,i : { 3 }", + "/voice0/trigger_value,f : { 1 }", + "/voice1/trigger_value,f : { 0.496063 }", + "/voice2/trigger_value,f : { 0.496063 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Triggers] sw_vel, with a note in between and sw_previous") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"( + key=60 sample=kick.wav + key=62 sw_previous=60 sw_vel=previous sample=snare.wav + key=64 sample=closedhat.wav + )"); + synth.noteOn(0, 60, 127); + synth.noteOn(5, 64, 63); + synth.noteOn(10, 62, 10); + synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr); + synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr); + std::vector expected { + "/num_active_voices,i : { 2 }", + "/voice0/trigger_value,f : { 1 }", + "/voice1/trigger_value,f : { 0.496063 }", + }; + REQUIRE(messageList == expected); +} diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 197fe80a..a26044ec 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -6,7 +6,6 @@ #include "TestHelpers.h" #include "sfizz/Synth.h" -#include "sfizz/Messaging.h" #include "catch2/catch.hpp" #include #include @@ -14,40 +13,6 @@ using namespace Catch::literals; using namespace sfz; -void simpleMessageReceiver(void* data, int delay, const char* path, const char* sig, const sfizz_arg_t* args) -{ - (void)delay; - auto& messageList = *reinterpret_cast*>(data); - - std::string newMessage = absl::StrCat(path, ",", sig, " : { "); - for (unsigned i = 0, n = strlen(sig); i < n; ++i) { - switch(sig[i]){ - case 'i': - absl::StrAppend(&newMessage, args[i].i); - break; - case 'f': - absl::StrAppend(&newMessage, args[i].f); - break; - case 'd': - absl::StrAppend(&newMessage, args[i].d); - break; - case 'h': - absl::StrAppend(&newMessage, args[i].h); - break; - case 's': - absl::StrAppend(&newMessage, args[i].s); - break; - } - - if (i == (n - 1)) - absl::StrAppend(&newMessage, " }"); - else - absl::StrAppend(&newMessage, ", "); - } - - messageList.push_back(std::move(newMessage)); -} - TEST_CASE("[Values] Delay") { Synth synth; diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index 0f3f76f6..6c572e7e 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -124,3 +124,37 @@ std::string createModulationDotGraph(std::vector lines) return graph; } + +void simpleMessageReceiver(void* data, int delay, const char* path, const char* sig, const sfizz_arg_t* args) +{ + (void)delay; + auto& messageList = *reinterpret_cast*>(data); + + std::string newMessage = absl::StrCat(path, ",", sig, " : { "); + for (unsigned i = 0, n = strlen(sig); i < n; ++i) { + switch(sig[i]){ + case 'i': + absl::StrAppend(&newMessage, args[i].i); + break; + case 'f': + absl::StrAppend(&newMessage, args[i].f); + break; + case 'd': + absl::StrAppend(&newMessage, args[i].d); + break; + case 'h': + absl::StrAppend(&newMessage, args[i].h); + break; + case 's': + absl::StrAppend(&newMessage, args[i].s); + break; + } + + if (i == (n - 1)) + absl::StrAppend(&newMessage, " }"); + else + absl::StrAppend(&newMessage, ", "); + } + + messageList.push_back(std::move(newMessage)); +} diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index 0c60fb91..feb6823f 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -9,6 +9,7 @@ #include "sfizz/Region.h" #include "sfizz/Voice.h" #include "sfizz/Range.h" +#include "sfizz/Messaging.h" #include "catch2/catch.hpp" #include "sfizz/modulations/ModKey.h" @@ -104,3 +105,8 @@ inline bool approxEqual(absl::Span lhs, absl::Span rhs, return true; } + +/** + * @brief Simple helper function that feeds all received messages into a std::vector* in data. + */ +void simpleMessageReceiver(void* data, int delay, const char* path, const char* sig, const sfizz_arg_t* args);