diff --git a/src/sfizz/Layer.cpp b/src/sfizz/Layer.cpp index 16a1b745..146d2181 100644 --- a/src/sfizz/Layer.cpp +++ b/src/sfizz/Layer.cpp @@ -69,7 +69,7 @@ bool Layer::registerNoteOn(int noteNumber, float velocity, float randValue) noex return false; if (region.velocityOverride == VelocityOverride::previous) - velocity = midiState_.getLastVelocity(); + velocity = midiState_.getVelocityOverride(); const bool velOk = region.velocityRange.containsWithEnd(velocity); const bool randOk = region.randRange.contains(randValue) || (randValue >= 1.0f && region.randRange.isValid() && region.randRange.getEnd() >= 1.0f); @@ -131,8 +131,6 @@ bool Layer::registerNoteOff(int noteNumber, float velocity, float randValue) noe bool Layer::registerCC(int ccNumber, float ccValue) noexcept { - ASSERT(ccValue >= 0.0f && ccValue <= 1.0f); - const Region& region = region_; if (ccNumber == region.sustainCC) diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index e077dfad..e82c3dba 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -19,11 +19,19 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex ASSERT(velocity >= 0 && velocity <= 1.0); if (noteNumber >= 0 && noteNumber < 128) { + velocityOverride = lastNoteVelocities[lastNotePlayed]; lastNoteVelocities[noteNumber] = velocity; noteOnTimes[noteNumber] = internalClock + static_cast(delay); lastNotePlayed = noteNumber; - activeNotes++; noteStates[noteNumber] = true; + ccEvent(delay, ExtendedCCs::noteOnVelocity, velocity); + ccEvent(delay, ExtendedCCs::keyboardNoteNumber, normalize7Bits(noteNumber)); + ccEvent(delay, ExtendedCCs::unipolarRandom, unipolarDist(Random::randomGenerator)); + ccEvent(delay, ExtendedCCs::bipolarRandom, bipolarDist(Random::randomGenerator)); + ccEvent(delay, ExtendedCCs::keyboardNoteGate, activeNotes > 0 ? 1.0f : 0.0f); + activeNotes++; + + ccEvent(delay, ExtendedCCs::alternate, alternate); alternate = alternate == 0.0f ? 1.0f : 0.0f; } @@ -37,6 +45,10 @@ void sfz::MidiState::noteOffEvent(int delay, int noteNumber, float velocity) noe UNUSED(velocity); if (noteNumber >= 0 && noteNumber < 128) { noteOffTimes[noteNumber] = internalClock + static_cast(delay); + ccEvent(delay, ExtendedCCs::noteOffVelocity, velocity); + ccEvent(delay, ExtendedCCs::keyboardNoteNumber, normalize7Bits(noteNumber)); + ccEvent(delay, ExtendedCCs::unipolarRandom, unipolarDist(Random::randomGenerator)); + ccEvent(delay, ExtendedCCs::bipolarRandom, bipolarDist(Random::randomGenerator)); if (activeNotes > 0) activeNotes--; noteStates[noteNumber] = false; @@ -121,9 +133,9 @@ float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept return lastNoteVelocities[noteNumber]; } -float sfz::MidiState::getLastVelocity() const noexcept +float sfz::MidiState::getVelocityOverride() const noexcept { - return lastNoteVelocities[lastNotePlayed]; + return velocityOverride; } void sfz::MidiState::insertEventInVector(EventVector& events, int delay, float value) @@ -172,7 +184,7 @@ float sfz::MidiState::getPolyAftertouch(int noteNumber) const noexcept { if (noteNumber < 0 || noteNumber > 127) return 0.0f; - + ASSERT(polyAftertouchEvents[noteNumber].size() > 0); return polyAftertouchEvents[noteNumber].back().value; } @@ -191,7 +203,7 @@ float sfz::MidiState::getCCValue(int ccNumber) const noexcept void sfz::MidiState::reset() noexcept { for (auto& velocity: lastNoteVelocities) - velocity = 0; + velocity = 0.0f; auto clearEvents = [] (EventVector& events) { events.clear(); @@ -207,6 +219,7 @@ void sfz::MidiState::reset() noexcept clearEvents(pitchEvents); clearEvents(channelAftertouchEvents); + velocityOverride = 0.0f; activeNotes = 0; internalClock = 0; lastNotePlayed = 0; diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index 3dab87bf..c92f45a4 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -86,11 +86,11 @@ public: float getNoteVelocity(int noteNumber) const noexcept; /** - * @brief Get the velocity of the last note played + * @brief Get the velocity override value (sw_vel in SFZ) * * @return float */ - float getLastVelocity() const noexcept; + float getVelocityOverride() const noexcept; /** * @brief Register a pitch bend event @@ -190,13 +190,6 @@ public: const EventVector& getPitchEvents() const noexcept; const EventVector& getChannelAftertouchEvents() const noexcept; - /** - * @brief Get the alternate state value, for extended CC 137 - * - * @return float - */ - float getAlternateState() const noexcept { return alternate; } - private: /** @@ -236,6 +229,11 @@ private: */ MidiNoteArray lastNoteVelocities; + /** + * @brief Velocity override value (sw_vel in SFZ) + */ + float velocityOverride; + /** * @brief Last note played */ @@ -272,5 +270,7 @@ private: int samplesPerBlock { config::defaultSamplesPerBlock }; float alternate { 0.0f }; unsigned internalClock { 0 }; + fast_real_distribution unipolarDist { 0.0f, 1.0f }; + fast_real_distribution bipolarDist { -1.0f, 1.0f }; }; } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 77173a4e..feb5621b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1073,8 +1073,8 @@ void Synth::hdNoteOn(int delay, int noteNumber, float normalizedVelocity) noexce ASSERT(noteNumber >= 0); Impl& impl = *impl_; ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration }; - impl.noteOnDispatch(delay, noteNumber, normalizedVelocity); impl.resources_.midiState.noteOnEvent(delay, noteNumber, normalizedVelocity); + impl.noteOnDispatch(delay, noteNumber, normalizedVelocity); } void Synth::noteOff(int delay, int noteNumber, int velocity) noexcept @@ -1087,20 +1087,19 @@ void Synth::hdNoteOff(int delay, int noteNumber, float normalizedVelocity) noexc { ASSERT(noteNumber < 128); ASSERT(noteNumber >= 0); - UNUSED(normalizedVelocity); Impl& impl = *impl_; ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration }; // 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? // auto replacedVelocity = (velocity == 0 ? getNoteVelocity(noteNumber) : velocity); + impl.resources_.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); const auto replacedVelocity = impl.resources_.midiState.getNoteVelocity(noteNumber); for (auto& voice : impl.voiceManager_) voice.registerNoteOff(delay, noteNumber, replacedVelocity); impl.noteOffDispatch(delay, noteNumber, replacedVelocity); - impl.resources_.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); } void Synth::Impl::startVoice(Layer* layer, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 14353f88..acd07c82 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -1454,6 +1454,16 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co } break; + MATCH("/voice&/remaining_delay", "") { + GET_VOICE_OR_BREAK(indices[0]) + client.receive<'i'>(delay, path, voice.getRemainingDelay()); + } break; + + MATCH("/voice&/source_position", "") { + GET_VOICE_OR_BREAK(indices[0]) + client.receive<'i'>(delay, path, voice.getSourcePosition()); + } break; + #undef MATCH // TODO... } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 64e6589a..10b69932 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -304,8 +304,6 @@ struct Voice::Impl PowerFollower powerFollower_; ExtendedCCValues extendedCCValues_; - fast_real_distribution unipolarDist { 0.0f, 1.0f }; - fast_real_distribution bipolarDist { -1.0f, 1.0f }; }; Voice::Voice(int voiceNumber, Resources& resources) @@ -389,10 +387,10 @@ const ExtendedCCValues& Voice::getExtendedCCValues() const noexcept void Voice::Impl::updateExtendedCCValues() noexcept { - extendedCCValues_.unipolar = unipolarDist(Random::randomGenerator); - extendedCCValues_.bipolar = bipolarDist(Random::randomGenerator); - extendedCCValues_.alternate = resources_.midiState.getAlternateState(); - extendedCCValues_.noteGate = resources_.midiState.getActiveNotes() > 0 ? 1.0f : 0.0f; + extendedCCValues_.unipolar = resources_.midiState.getCCValue(ExtendedCCs::unipolarRandom); + extendedCCValues_.bipolar = resources_.midiState.getCCValue(ExtendedCCs::bipolarRandom); + extendedCCValues_.alternate = resources_.midiState.getCCValue(ExtendedCCs::alternate); + extendedCCValues_.noteGate = resources_.midiState.getCCValue(ExtendedCCs::keyboardNoteGate); } bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexcept @@ -410,7 +408,7 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc impl.triggerEvent_.number = region.pitchKeycenter; if (region.velocityOverride == VelocityOverride::previous) - impl.triggerEvent_.value = resources.midiState.getLastVelocity(); + impl.triggerEvent_.value = resources.midiState.getVelocityOverride(); if (region.disabled()) { impl.switchState(State::cleanMeUp); @@ -2000,6 +1998,18 @@ const Region* Voice::getRegion() const noexcept return impl.region_; } +int Voice::getRemainingDelay() const noexcept +{ + Impl& impl = *impl_; + return impl.initialDelay_; +} + +int Voice::getSourcePosition() const noexcept +{ + Impl& impl = *impl_; + return impl.sourcePosition_; +} + LFO* Voice::getLFO(size_t index) { Impl& impl = *impl_; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 9e7f3593..0643004f 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -416,6 +416,19 @@ public: */ const ExtendedCCValues& getExtendedCCValues() const noexcept; + /** + * @brief Get the remaining delay before the sample starts, in samples + * + * @return int + */ + int getRemainingDelay() const noexcept; + /** + * @brief Get the current position in the source sample + * + * @return int + */ + int getSourcePosition() const noexcept; + public: /** * @brief Check if the voice already belongs to a sister ring diff --git a/tests/MidiStateT.cpp b/tests/MidiStateT.cpp index 5bec78ee..8bb44d40 100644 --- a/tests/MidiStateT.cpp +++ b/tests/MidiStateT.cpp @@ -11,9 +11,11 @@ */ #include "sfizz/MidiState.h" +#include "sfizz/Synth.h" #include "sfizz/SfzHelpers.h" #include "catch2/catch.hpp" #include "absl/strings/string_view.h" +#include "TestHelpers.h" using namespace Catch::literals; using namespace sfz::literals; @@ -88,5 +90,150 @@ TEST_CASE("[MidiState] Last note velocity") sfz::MidiState state; state.noteOnEvent(0, 62, 64_norm); state.noteOnEvent(0, 60, 10_norm); - REQUIRE(state.getLastVelocity() == 10_norm); + REQUIRE(state.getVelocityOverride() == 64_norm); +} + + +TEST_CASE("[CC] Extended CCs on offset and delay") +{ + sfz::Synth synth; + std::vector messageList; + sfz::Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + synth.setSampleRate(48000); + + SECTION("CC131 - Note on velocity") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/extended_ccs.sfz", R"( + key=60 delay_cc131=1 sample=kick.wav + key=61 offset_cc131=100 sample=snare.wav + )"); + synth.hdNoteOn(0, 60, 0.0f); + synth.hdNoteOn(0, 60, 0.5f); + synth.hdNoteOn(0, 61, 0.0f); + synth.hdNoteOn(0, 61, 0.5f); + synth.dispatchMessage(client, 0, "/voice0/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice2/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice3/source_position", "", nullptr); + std::vector expected { + "/voice0/remaining_delay,i : { 0 }", + "/voice1/remaining_delay,i : { 24000 }", + "/voice2/source_position,i : { 0 }", + "/voice3/source_position,i : { 50 }", + }; + REQUIRE(messageList == expected); + } + + SECTION("CC132 - Note off velocity") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/extended_ccs.sfz", R"( + key=60 sample=*silence + key=60 delay_cc132=1 sample=kick.wav trigger=release + key=61 sample=snare.wav + key=61 offset_cc132=100 sample=snare.wav trigger=release + )"); + synth.hdNoteOn(0, 60, 1.0f); + synth.hdNoteOff(1, 60, 0.0f); + synth.hdNoteOn(2, 60, 1.0f); + synth.hdNoteOff(3, 60, 0.5f); + synth.hdNoteOn(4, 61, 1.0f); + synth.hdNoteOff(5, 61, 0.0f); + synth.hdNoteOn(6, 61, 1.0f); + synth.hdNoteOff(7, 61, 0.5f); + synth.dispatchMessage(client, 10, "/voice1/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 10, "/voice3/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 10, "/voice5/source_position", "", nullptr); + synth.dispatchMessage(client, 10, "/voice7/source_position", "", nullptr); + std::vector expected { + "/voice1/remaining_delay,i : { 1 }", // 1 is the note off event delay + "/voice3/remaining_delay,i : { 24003 }", // 3 is the note off event delay + "/voice5/source_position,i : { 0 }", + "/voice7/source_position,i : { 50 }", + }; + REQUIRE(messageList == expected); + } + + SECTION("CC133 - Note number") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/extended_ccs.sfz", R"( + delay_cc133=1 offset_cc133=100 sample=kick.wav + )"); + synth.hdNoteOn(0, 0, 1.0f); + synth.hdNoteOn(0, 127, 1.0f); + synth.dispatchMessage(client, 0, "/voice0/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice0/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/source_position", "", nullptr); + std::vector expected { + "/voice0/remaining_delay,i : { 0 }", + "/voice1/remaining_delay,i : { 48000 }", + "/voice0/source_position,i : { 0 }", + "/voice1/source_position,i : { 100 }", + }; + REQUIRE(messageList == expected); + } + + SECTION("CC134 - Note gate") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/extended_ccs.sfz", R"( + delay_cc134=1 offset_cc134=100 sample=kick.wav + )"); + synth.hdNoteOn(0, 60, 1.0f); + synth.hdNoteOn(0, 127, 1.0f); + synth.hdNoteOff(1, 60, 1.0f); + synth.hdNoteOff(1, 127, 1.0f); + synth.hdNoteOn(2, 60, 1.0f); + synth.hdNoteOn(2, 127, 1.0f); + synth.dispatchMessage(client, 0, "/voice0/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice2/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice3/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice0/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice2/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice3/source_position", "", nullptr); + std::vector expected { + "/voice0/remaining_delay,i : { 0 }", + "/voice1/remaining_delay,i : { 48000 }", + "/voice2/remaining_delay,i : { 2 }", // 2 is the event delay + "/voice3/remaining_delay,i : { 48002 }", // 2 is the event delay + "/voice0/source_position,i : { 0 }", + "/voice1/source_position,i : { 100 }", + "/voice2/source_position,i : { 0 }", + "/voice3/source_position,i : { 100 }", + }; + REQUIRE(messageList == expected); + } + + SECTION("CC137 - Alternate") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/extended_ccs.sfz", R"( + delay_cc137=1 offset_cc137=100 sample=kick.wav + )"); + synth.hdNoteOn(0, 60, 1.0f); + synth.hdNoteOn(0, 127, 1.0f); + synth.hdNoteOn(0, 54, 1.0f); + synth.hdNoteOn(0, 12, 1.0f); + synth.dispatchMessage(client, 0, "/voice0/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice2/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice3/remaining_delay", "", nullptr); + synth.dispatchMessage(client, 0, "/voice0/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice1/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice2/source_position", "", nullptr); + synth.dispatchMessage(client, 0, "/voice3/source_position", "", nullptr); + std::vector expected { + "/voice0/remaining_delay,i : { 0 }", + "/voice1/remaining_delay,i : { 48000 }", + "/voice2/remaining_delay,i : { 0 }", + "/voice3/remaining_delay,i : { 48000 }", + "/voice0/source_position,i : { 0 }", + "/voice1/source_position,i : { 100 }", + "/voice2/source_position,i : { 0 }", + "/voice3/source_position,i : { 100 }", + }; + REQUIRE(messageList == expected); + } }