Merge pull request #650 from paulfd/sw-vel

Add sw_vel
This commit is contained in:
Paul Ferrand 2021-02-23 18:37:12 +01:00 committed by GitHub
commit 7782ded3e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 260 additions and 43 deletions

View file

@ -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<unsigned>(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);
}

View file

@ -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<float> lastNoteVelocities;
/**
* @brief Last note played
*/
int lastNotePlayed { 0 };
/**
* @brief Current known values for the CCs.
*

View file

@ -1484,6 +1484,9 @@ bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue
if (!triggerOnNote)
return false;
if (velocityOverride == VelocityOverride::previous)
velocity = midiState.getLastVelocity();
const bool velOk = velocityRange.containsWithEnd(velocity);
const bool randOk = randRange.contains(randValue) || (randValue == 1.0f && randRange.getEnd() == 1.0f);
const bool firstLegatoNote = (trigger == Trigger::first && midiState.getActiveNotes() == 1);

View file

@ -998,8 +998,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
@ -1010,7 +1010,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?
@ -1021,6 +1020,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
@ -1061,7 +1061,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) {
@ -1089,6 +1088,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);
}
}
@ -1150,7 +1153,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);
@ -1172,6 +1174,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)

View file

@ -1210,6 +1210,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<int>(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...
}

View file

@ -82,3 +82,11 @@ TEST_CASE("[MidiState] Extended CCs")
sfz::MidiState state;
state.ccEvent(0, 142, 64_norm); // should not trap
}
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);
}

View file

@ -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,140 @@ TEST_CASE("Legato triggers", "Region triggers")
REQUIRE(!region.registerNoteOn(42, 64_norm, 0.5f));
}
}
TEST_CASE("[Triggers] sw_vel, basic")
{
Synth synth;
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"(
<region> key=60 sample=kick.wav
<region> 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<std::string> 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<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"(
<region> key=60 sample=kick.wav
<region> 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<std::string> 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<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"(
<region> key=60 sample=kick.wav
<region> key=62 sw_vel=previous sample=snare.wav
<region> 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<std::string> 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<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"(
<region> key=60 sample=kick.wav
<region> key=62 sw_previous=60 sw_vel=previous sample=snare.wav
<region> 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<std::string> expected {
"/num_active_voices,i : { 2 }",
"/voice0/trigger_value,f : { 1 }",
"/voice1/trigger_value,f : { 0.496063 }",
};
REQUIRE(messageList == expected);
}
TEST_CASE("[Triggers] sw_vel, consider the previous velocity for triggers")
{
Synth synth;
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_vel.sfz", R"(
<region> key=60 sample=kick.wav
<region> key=62 sw_previous=60 sw_vel=previous sample=snare.wav lovel=63
)");
SECTION("Should trigger") {
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<std::string> expected {
"/num_active_voices,i : { 2 }",
"/voice0/trigger_value,f : { 1 }",
"/voice1/trigger_value,f : { 1 }",
};
REQUIRE(messageList == expected);
}
SECTION("Should not trigger") {
synth.noteOn(0, 60, 10);
synth.noteOn(10, 62, 127);
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<std::string> expected {
"/num_active_voices,i : { 1 }",
"/voice0/trigger_value,f : { 0.0787402 }",
};
REQUIRE(messageList == expected);
}
}

View file

@ -6,7 +6,6 @@
#include "TestHelpers.h"
#include "sfizz/Synth.h"
#include "sfizz/Messaging.h"
#include "catch2/catch.hpp"
#include <stdexcept>
#include <absl/strings/str_cat.h>
@ -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<std::vector<std::string>*>(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;

View file

@ -124,3 +124,37 @@ std::string createModulationDotGraph(std::vector<std::string> 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<std::vector<std::string>*>(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));
}

View file

@ -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<const Type> lhs, absl::Span<const Type> rhs,
return true;
}
/**
* @brief Simple helper function that feeds all received messages into a std::vector<std::string>* in data.
*/
void simpleMessageReceiver(void* data, int delay, const char* path, const char* sig, const sfizz_arg_t* args);