Add sw_vel
Also add some introspection for voices using OSC and tests
This commit is contained in:
parent
08558ceae2
commit
c118ad6cbe
8 changed files with 210 additions and 43 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<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...
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue