Test and API doc update w.r.t. ordering

Updated tests mostly to call on `renderBlock()` before testing things like number of active voices and such, in case we buffer/reorder events in the library later on.
This commit is contained in:
Paul Fd 2021-04-05 00:18:33 +02:00
parent d0cd21acf9
commit cfb9eae68f
10 changed files with 611 additions and 294 deletions

View file

@ -325,11 +325,12 @@ SFIZZ_EXPORTED_API void sfizz_set_sample_rate(sfizz_synth_t* synth, float sample
/**
* @brief Send a note on event to the synth.
*
* As with all MIDI events, this needs to happen before the call to
* sfizz_render_block() in each block and should appear in order of the delays.
* @since 0.2.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay of the event in the block, in samples.
* @param note_number The MIDI note number.
@ -342,12 +343,14 @@ SFIZZ_EXPORTED_API void sfizz_send_note_on(sfizz_synth_t* synth, int delay, int
/**
* @brief Send a note off event to the synth.
* @since 0.2.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* As with all MIDI events, this needs to happen before the call to
* sfizz_render_block() in each block and should appear in order of the delays.
* As per the SFZ spec the velocity of note-off events is usually replaced by
* the note-on velocity.
* @since 0.2.0
*
* @param synth The synth.
* @param delay The delay of the event in the block, in samples.
@ -361,11 +364,12 @@ SFIZZ_EXPORTED_API void sfizz_send_note_off(sfizz_synth_t* synth, int delay, int
/**
* @brief Send a CC event to the synth.
*
* As with all MIDI events, this needs to happen before the call to
* sfizz_render_block() in each block and should appear in order of the delays.
* @since 0.2.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay of the event in the block, in samples.
* @param cc_number The MIDI CC number.
@ -378,11 +382,12 @@ SFIZZ_EXPORTED_API void sfizz_send_cc(sfizz_synth_t* synth, int delay, int cc_nu
/**
* @brief Send a high precision CC event to the synth.
*
* As with all MIDI events, this needs to happen before the call to
* sfizz_render_block() in each block and should appear in order of the delays.
* @since 0.4.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay of the event in the block, in samples.
* @param cc_number The MIDI CC number.
@ -395,13 +400,14 @@ SFIZZ_EXPORTED_API void sfizz_send_hdcc(sfizz_synth_t* synth, int delay, int cc_
/**
* @brief Send a high precision CC automation to the synth.
* @since 0.6.0
*
* This updates the CC value known to the synth, but without performing
* additional MIDI-specific interpretations. (eg. the CC 120 and up)
*
* As with all MIDI events, this needs to happen before the call to
* sfizz_render_block() in each block and should appear in order of the delays.
* @since 0.6.0
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay of the event in the block, in samples.
@ -415,11 +421,12 @@ SFIZZ_EXPORTED_API void sfizz_automate_hdcc(sfizz_synth_t* synth, int delay, int
/**
* @brief Send a pitch wheel event.
*
* As with all MIDI events, this needs to happen before the call to
* sfizz_render_block() in each block and should appear in order of the delays.
* @since 0.4.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay.
* @param pitch The pitch.
@ -433,6 +440,10 @@ SFIZZ_EXPORTED_API void sfizz_send_pitch_wheel(sfizz_synth_t* synth, int delay,
* @brief Send an aftertouch event.
* @since 0.2.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay at which the event occurs; this should be lower
* than the size of the block in the next call to renderBlock().
@ -448,6 +459,10 @@ SFIZZ_EXPORTED_API void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, c
* in the internal engine.
* @since 0.6.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay at which the event occurs; this should be lower
* than the size of the block in the next call to renderBlock().
@ -461,6 +476,11 @@ SFIZZ_EXPORTED_API void sfizz_send_poly_aftertouch(sfizz_synth_t* synth, int del
/**
* @brief Send a tempo event.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.2.0
*
* @param synth The synth.
@ -474,6 +494,11 @@ SFIZZ_EXPORTED_API void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float
/**
* @brief Send the time signature.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.5.0
*
* @param synth The synth.
@ -488,6 +513,11 @@ SFIZZ_EXPORTED_API void sfizz_send_time_signature(sfizz_synth_t* synth, int dela
/**
* @brief Send the time position.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.5.0
*
* @param synth The synth.
@ -502,6 +532,11 @@ SFIZZ_EXPORTED_API void sfizz_send_time_position(sfizz_synth_t* synth, int delay
/**
* @brief Send the playback state.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.5.0
*
* @param synth The synth.
@ -521,6 +556,7 @@ SFIZZ_EXPORTED_API void sfizz_send_playback_state(sfizz_synth_t* synth, int dela
* for the block (midi notes, CCs, ...) before rendering each block.
* The synth will memorize the inputs and render sample accurates envelopes
* depending on the input events passed to it.
*
* @since 0.2.0
*
* @param synth The synth.

View file

@ -334,9 +334,12 @@ public:
/**
* @brief Send a note on event to the synth.
*
* @since 0.2.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param delay the delay at which the event occurs; this should be lower
* than the size of the block in the next call to renderBlock().
* @param noteNumber the midi note number.
@ -349,9 +352,12 @@ public:
/**
* @brief Send a note off event to the synth.
*
* @since 0.2.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param delay the delay at which the event occurs; this should be lower
* than the size of the block in the next call to renderBlock().
* @param noteNumber the midi note number.
@ -364,9 +370,12 @@ public:
/**
* @brief Send a CC event to the synth
*
* @since 0.2.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param delay the delay at which the event occurs; this should be lower
* than the size of the block in the next call to renderBlock().
* @param ccNumber the cc number.
@ -379,9 +388,12 @@ public:
/**
* @brief Send a high precision CC event to the synth
*
* @since 0.4.0
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param delay the delay at which the event occurs; this should be lower
* than the size of the block in the next call to renderBlock().
* @param ccNumber the cc number.
@ -398,6 +410,10 @@ public:
* This updates the CC value known to the synth, but without performing
* additional MIDI-specific interpretations. (eg. the CC 120 and up)
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @since 0.6.0
*
* @param delay the delay at which the event occurs; this should be lower
@ -413,6 +429,10 @@ public:
/**
* @brief Send a pitch bend event to the synth
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @since 0.2.0
*
* @param delay the delay at which the event occurs; this should be lower
@ -427,6 +447,10 @@ public:
/**
* @brief Send an aftertouch event to the synth.
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @since 0.2.0
*
* @param delay the delay at which the event occurs; this should be lower
@ -442,6 +466,10 @@ public:
* @brief Send a polyphonic aftertouch event to the synth. This feature is
* experimental and needs more testing in the internal engine.
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @since 0.6.0
*
* @param delay the delay at which the event occurs; this should be lower
@ -457,6 +485,10 @@ public:
/**
* @brief Send a tempo event to the synth.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.2.0
*
* @param delay the delay at which the event occurs; this should be lower
@ -471,6 +503,10 @@ public:
/**
* @brief Send the time signature.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.5.0
*
* @param delay The delay.
@ -485,6 +521,10 @@ public:
/**
* @brief Send the time position.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.5.0
*
* @param delay The delay.
@ -499,6 +539,10 @@ public:
/**
* @brief Send the playback state.
*
* This command should be delay-ordered with all other time/signature commands, namely
* tempo(), timeSignature(), timePosition(), and playbackState(), otherwise the behavior
* of the synth is undefined.
*
* @since 0.5.0
*
* @param delay The delay.

View file

@ -417,7 +417,8 @@ public:
* @brief Send a tempo event to the synth
*
* @param delay the delay at which the event occurs; this should be lower than the size of
* the block in the next call to renderBlock().
* the block in the next call to renderBlock(), and ordered with respect to
* calls to tempo(), timeSignature(), timePosition(), and playbackState().
* @param secondsPerQuarter the new period of the quarter note
*/
void tempo(int delay, float secondsPerQuarter) noexcept;
@ -438,28 +439,35 @@ public:
*/
void hdPolyAftertouch(int delay, int noteNumber, float normAftertouch) noexcept;
/**
* @brief Send the time signature.
* @brief Send the time signature.
*
* @param delay The delay.
* @param beats_per_bar The number of beats per bar, or time signature numerator.
* @param beat_unit The note corresponding to one beat, or time signature denominator.
* @param delay the delay at which the event occurs; this should be lower than the size of
* the block in the next call to renderBlock(), and ordered with respect to
* calls to tempo(), timeSignature(), timePosition(), and playbackState().
* @param beats_per_bar The number of beats per bar, or time signature numerator.
* @param beat_unit The note corresponding to one beat, or time signature denominator.
*/
void timeSignature(int delay, int beatsPerBar, int beatUnit);
/**
* @brief Send the time position.
* @brief Send the time position.
*
* @param delay The delay.
* @param bar The current bar.
* @param bar_beat The fractional position of the current beat within the bar.
* @param delay the delay at which the event occurs; this should be lower than the size of
* the block in the next call to renderBlock(), and ordered with respect to
* calls to tempo(), timeSignature(), timePosition(), and playbackState().
* @param bar The current bar.
* @param bar_beat The fractional position of the current beat within the bar.
*/
void timePosition(int delay, int bar, double barBeat);
/**
* @brief Send the playback state.
* @brief Send the playback state.
*
* @param delay The delay.
* @param playback_state The playback state, 1 if playing, 0 if stopped.
* @param delay the delay at which the event occurs; this should be lower than the size of
* the block in the next call to renderBlock(), and ordered with respect to
* calls to tempo(), timeSignature(), timePosition(), and playbackState().
* @param playback_state The playback state, 1 if playing, 0 if stopped.
*/
void playbackState(int delay, int playbackState);
/**
* @brief Render an block of audio data in the buffer. This call will reset
* the synth in its waiting state for the next batch of events. The size of
@ -534,16 +542,16 @@ public:
uint32_t getPreloadSize() const noexcept;
/**
* @brief Gets the number of allocated buffers.
* @brief Gets the number of allocated buffers.
*
* @return The allocated buffers.
* @return The allocated buffers.
*/
int getAllocatedBuffers() const noexcept { return Buffer<float>::counter().getNumBuffers(); }
/**
* @brief Gets the number of bytes allocated through the buffers
* @brief Gets the number of bytes allocated through the buffers
*
* @return The allocated bytes.
* @return The allocated bytes.
*/
int getAllocatedBytes() const noexcept { return Buffer<float>::counter().getTotalBytes(); }
@ -610,15 +618,15 @@ public:
void allSoundOff() noexcept;
/**
* @brief Get the parser.
* @brief Get the parser.
*
* @return A reference to the parser.
* @return A reference to the parser.
*/
Parser& getParser() noexcept;
/**
* @brief Get the parser.
* @brief Get the parser.
*
* @return A reference to the parser.
* @return A reference to the parser.
*/
const Parser& getParser() const noexcept;

View file

@ -500,10 +500,14 @@ TEST_CASE("[Files] Note and octave offsets")
TEST_CASE("[Files] Off modes")
{
Synth synth;
AudioBuffer<float> buffer { 2, 256 };
synth.setSamplesPerBlock(256);
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_mode.sfz");
REQUIRE( synth.getNumRegions() == 3 );
synth.noteOn(0, 64, 63);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );
const auto* fastVoice =
synth.getVoiceView(0)->getRegion()->offMode == OffMode::fast ?
@ -514,9 +518,10 @@ TEST_CASE("[Files] Off modes")
synth.getVoiceView(1) :
synth.getVoiceView(0) ;
synth.noteOn(100, 63, 63);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 1 );
AudioBuffer<float> buffer { 2, 256 };
for (unsigned i = 0; i < 20; ++i) // Not enough for the "normal" voice to die
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );

View file

@ -17,8 +17,6 @@
using namespace Catch::literals;
using namespace sfz::literals;
constexpr int blockSize { 256 };
TEST_CASE("[Polyphony] Polyphony in hierarchy")
{
sfz::Synth synth;
@ -76,72 +74,72 @@ TEST_CASE("[Polyphony] Polyphony groups")
TEST_CASE("[Polyphony] group polyphony limits")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> group=1 polyphony=2
<region> sample=*sine key=65
)");
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 65, 64);
synth.noteOn(2, 65, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> polyphony=2
<region> sample=*sine key=65
)");
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 65, 64);
synth.noteOn(2, 65, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> polyphony=2
<region> sample=*sine key=65
)");
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 65, 64);
synth.noteOn(2, 65, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<master> polyphony=2
<group> polyphony=5
<region> sample=*sine key=65
)");
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 65, 64);
synth.noteOn(2, 65, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<master> polyphony=2
<region> sample=*saw key=65
@ -150,38 +148,37 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)")
<region> sample=*sine key=66
)");
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 66, 64);
synth.noteOn(0, 66, 64);
synth.noteOn(0, 66, 64);
REQUIRE( synth.getNumActiveVoices() == 6);
synth.noteOn(1, 65, 64);
synth.noteOn(2, 65, 64);
synth.noteOn(3, 66, 64);
synth.noteOn(4, 66, 64);
synth.noteOn(5, 66, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 6);
REQUIRE( numPlayingVoices(synth) == 5); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<global> polyphony=2
<group> polyphony=5
<region> sample=*sine key=65
)");
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 65, 64);
synth.noteOn(2, 65, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Polyphony in master")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.setSamplesPerBlock(blockSize);
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<master> polyphony=2
<group> group=2
@ -192,28 +189,28 @@ TEST_CASE("[Polyphony] Polyphony in master")
<region> sample=*sine key=61
)");
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 65, 64);
synth.noteOn(2, 65, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
synth.allSoundOff();
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 0);
synth.noteOn(0, 63, 64);
synth.noteOn(0, 63, 64);
synth.noteOn(0, 63, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 63, 64);
synth.noteOn(2, 63, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
synth.allSoundOff();
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 0);
synth.noteOn(0, 61, 64);
synth.noteOn(0, 61, 64);
synth.noteOn(0, 61, 64);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.noteOn(1, 61, 64);
synth.noteOn(2, 61, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 3 );
}
@ -221,15 +218,15 @@ TEST_CASE("[Polyphony] Polyphony in master")
TEST_CASE("[Polyphony] Self-masking")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine key=64 note_polyphony=2
)");
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 64);
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
synth.noteOn(1, 64, 62 );
synth.noteOn(2, 64, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
@ -242,15 +239,15 @@ TEST_CASE("[Polyphony] Self-masking")
TEST_CASE("[Polyphony] Not self-masking")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine key=66 note_polyphony=2 note_selfmask=off
)");
synth.noteOn(0, 66, 63 );
synth.noteOn(0, 66, 62 );
synth.noteOn(0, 66, 64);
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
synth.noteOn(1, 66, 62 );
synth.noteOn(2, 66, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
@ -263,15 +260,15 @@ TEST_CASE("[Polyphony] Not self-masking")
TEST_CASE("[Polyphony] Self-masking with the exact same velocity")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path(), R"(
<region> sample=*sine key=64 note_polyphony=2
)");
synth.noteOn(0, 64, 64);
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 63 );
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
synth.noteOn(1, 64, 63 );
synth.noteOn(2, 64, 63 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
@ -284,11 +281,13 @@ TEST_CASE("[Polyphony] Self-masking with the exact same velocity")
TEST_CASE("[Polyphony] Self-masking only works from low to high")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine key=64 note_polyphony=1
)");
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 62 );
synth.noteOn(1, 64, 62 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 ); // Both notes are playing
REQUIRE( numPlayingVoices(synth) == 2 ); // id
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
@ -300,14 +299,13 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high")
TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*saw key=64 note_polyphony=1
<region> sample=*sine key=64 note_polyphony=1
)");
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 63 );
REQUIRE( synth.getNumActiveVoices() == 4);
synth.noteOn(1, 64, 63 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
@ -323,20 +321,21 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po
TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default, with keyswitches)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<global> sw_lokey=36 sw_hikey=37 sw_default=36
<region> sw_last=36 key=48 note_polyphony=1 sample=*saw
<region> sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri
)");
synth.noteOn(0, 48, 63 );
REQUIRE( synth.getNumActiveVoices() == 1);
synth.cc(0, 64, 127);
synth.noteOn(0, 37, 127);
synth.noteOff(0, 37, 0);
synth.noteOn(0, 48, 64);
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.cc(1, 64, 127);
synth.noteOn(2, 37, 127);
synth.noteOff(3, 37, 0);
synth.noteOn(4, 48, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
@ -348,15 +347,15 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po
TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> group=1 sample=*saw key=64 note_polyphony=1
<region> group=2 sample=*sine key=64 note_polyphony=1
)");
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 63 );
REQUIRE( synth.getNumActiveVoices() == 4); // Both notes are playing
synth.noteOn(1, 64, 63 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 4); // Both notes are playing
REQUIRE(numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed
@ -371,20 +370,21 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups")
TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (with keyswitches)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<global> sw_lokey=36 sw_hikey=37 sw_default=36
<region> group=1 sw_last=36 key=48 note_polyphony=1 sample=*saw
<region> group=2 sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri
)");
synth.noteOn(0, 48, 63 );
REQUIRE( synth.getNumActiveVoices() == 1);
synth.cc(0, 64, 127);
synth.noteOn(0, 37, 127);
synth.noteOff(0, 37, 0);
synth.noteOn(0, 48, 64);
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.cc(1, 64, 127);
synth.noteOn(2, 37, 127);
synth.noteOff(3, 37, 0);
synth.noteOn(4, 48, 64);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE(numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
@ -395,17 +395,18 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi
TEST_CASE("[Polyphony] Note polyphony operates on release voices")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 note_polyphony=1 sample=*saw trigger=release_key ampeg_attack=1 ampeg_decay=1
)");
synth.noteOn(0, 48, 63 );
synth.noteOff(10, 48, 0 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.noteOn(20, 48, 65 );
synth.noteOff(30, 48, 10 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE(numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
@ -416,16 +417,18 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices")
TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works from low to high but takes into account the replaced velocity)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 note_polyphony=1 sample=*saw trigger=release_key ampeg_attack=1 ampeg_decay=1
)");
synth.noteOn(0, 48, 63 );
synth.noteOff(10, 48, 0 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 1);
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(20, 48, 61 );
synth.noteOff(30, 48, 10 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
@ -437,7 +440,7 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works
TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 sample=*sine
<region> key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1
@ -449,37 +452,21 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain ped
synth.noteOff(3, 48, 0 );
synth.noteOn(4, 48, 63 );
synth.noteOff(5, 48, 0 );
REQUIRE( synth.getNumActiveVoices() == 3);
REQUIRE( synth.getVoiceView(0)->getRegion()->sampleId->filename() == "*sine");
REQUIRE( synth.getVoiceView(1)->getRegion()->sampleId->filename() == "*sine");
REQUIRE( synth.getVoiceView(2)->getRegion()->sampleId->filename() == "*sine");
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 3 );
std::vector<std::string> expectedSamples { "*sine", "*sine", "*sine" };
REQUIRE( playingSamples(synth) == expectedSamples );
synth.cc(20, 64, 0);
REQUIRE( synth.getNumActiveVoices() == 6 );
REQUIRE( synth.getVoiceView(3)->getRegion()->sampleId->filename() == "*saw");
REQUIRE( synth.getVoiceView(4)->getRegion()->sampleId->filename() == "*saw");
REQUIRE( synth.getVoiceView(5)->getRegion()->sampleId->filename() == "*saw");
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 61_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree());
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(2)->releasedOrFree());
REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 61_norm);
REQUIRE( synth.getVoiceView(3)->releasedOrFree());
REQUIRE( synth.getVoiceView(4)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(4)->releasedOrFree());
REQUIRE( synth.getVoiceView(5)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(5)->releasedOrFree());
std::vector<std::string> expectedSamples2 { "*saw" };
std::vector<float> expectedVelocities { 63_norm };
REQUIRE( playingSamples(synth) == expectedSamples2 );
REQUIRE( playingVelocities(synth) == expectedVelocities );
}
TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal (masking)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 sample=*sine
<region> key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1
@ -491,75 +478,53 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain ped
synth.noteOff(3, 48, 0 );
synth.noteOn(4, 48, 61 );
synth.noteOff(5, 48, 0 );
REQUIRE( synth.getNumActiveVoices() == 3);
REQUIRE( synth.getVoiceView(0)->getRegion()->sampleId->filename() == "*sine");
REQUIRE( synth.getVoiceView(1)->getRegion()->sampleId->filename() == "*sine");
REQUIRE( synth.getVoiceView(2)->getRegion()->sampleId->filename() == "*sine");
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 3 );
std::vector<std::string> expectedSamples { "*sine", "*sine", "*sine" };
REQUIRE( playingSamples(synth) == expectedSamples );
synth.cc(20, 64, 0);
REQUIRE( synth.getNumActiveVoices() == 6 );
REQUIRE( synth.getVoiceView(3)->getRegion()->sampleId->filename() == "*saw");
REQUIRE( synth.getVoiceView(4)->getRegion()->sampleId->filename() == "*saw");
REQUIRE( synth.getVoiceView(5)->getRegion()->sampleId->filename() == "*saw");
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 3 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree());
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 61_norm);
REQUIRE( synth.getVoiceView(2)->releasedOrFree());
REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(3)->releasedOrFree());
REQUIRE( synth.getVoiceView(4)->getTriggerEvent().value == 62_norm);
REQUIRE(!synth.getVoiceView(4)->releasedOrFree());
REQUIRE( synth.getVoiceView(5)->getTriggerEvent().value == 61_norm);
REQUIRE(!synth.getVoiceView(5)->releasedOrFree());
std::vector<std::string> expectedSamples2 { "*saw", "*saw", "*saw" };
std::vector<float> expectedVelocities { 63_norm, 62_norm, 61_norm };
REQUIRE( playingSamples(synth) == expectedSamples2 );
REQUIRE( playingVelocities(synth) == expectedVelocities );
}
TEST_CASE("[Polyphony] Bi-directional choking (with polyphony)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> key=60 polyphony=1
<region> sample=kick.wav loop_mode=one_shot
<region> sample=snare.wav trigger=release
)");
synth.noteOn(0, 60, 63 );
REQUIRE( synth.getNumActiveVoices() == 1);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId->filename() == "kick.wav" );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "kick.wav" } );
synth.noteOff(10, 60, 63 );
REQUIRE( synth.getNumActiveVoices() == 2);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId->filename() == "snare.wav" );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "snare.wav" } );
synth.noteOn(20, 60, 63 );
REQUIRE( synth.getNumActiveVoices() == 3);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId->filename() == "kick.wav" );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "kick.wav" } );
}
TEST_CASE("[Polyphony] Bi-directional choking (with note_polyphony)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> key=60 note_polyphony=1
<region> sample=kick.wav loop_mode=one_shot
<region> sample=snare.wav trigger=release
)");
synth.noteOn(0, 60, 63 );
REQUIRE( synth.getNumActiveVoices() == 1);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId->filename() == "kick.wav" );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "kick.wav" } );
synth.noteOff(10, 60, 63 );
REQUIRE( synth.getNumActiveVoices() == 2);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId->filename() == "snare.wav" );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "snare.wav" } );
synth.noteOn(20, 60, 63 );
REQUIRE( synth.getNumActiveVoices() == 3);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId->filename() == "kick.wav" );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "kick.wav" } );
}

View file

@ -190,159 +190,200 @@ TEST_CASE("Region activation", "Region tests")
TEST_CASE("[Keyswitches] Normal lastKeyswitch range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<global> sw_lokey=40 sw_hikey=42 sw_default=40
<region> sw_last=40 key=60 sample=*sine
<region> sw_last=41 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 41, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] No lastKeyswitch range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<region> sw_last=40 key=60 sample=*sine
<region> sw_last=41 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 41, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] Out of lastKeyswitch range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<global> sw_lokey=40 sw_hikey=42 sw_default=40
<region> sw_last=40 key=60 sample=*sine
<region> sw_last=43 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 43, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] Overlapping key and lastKeyswitch range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<global> sw_lokey=1 sw_hikey=127 sw_default=40
<region> sw_last=40 key=60 sample=*sine
<region> sw_last=41 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 41, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
synth.noteOn(0, 43, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 3);
}
TEST_CASE("[Keyswitches] sw_down, in range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<global> sw_lokey=1 sw_hikey=127 sw_default=40
<region> sw_down=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
}
TEST_CASE("[Keyswitches] sw_down, out of range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<global> sw_lokey=1 sw_hikey=10 sw_default=40
<region> sw_down=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
}
TEST_CASE("[Keyswitches] sw_up, in range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<global> sw_lokey=1 sw_hikey=127 sw_default=40
<region> sw_up=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] sw_up, out of range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/keyswitches.sfz", R"(
<global> sw_lokey=1 sw_hikey=127 sw_default=40
<region> sw_up=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] sw_default")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_default.sfz", R"(
<global> sw_lokey=30 sw_hikey=50 sw_default=40
<region> sw_last=41 key=51 sample=*sine
@ -360,6 +401,7 @@ TEST_CASE("[Keyswitches] sw_default")
TEST_CASE("[Keyswitches] sw_default and playing with switches")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_default.sfz", R"(
<global> sw_lokey=30 sw_hikey=50 sw_default=40
<region> sw_last=41 key=51 sample=*sine
@ -374,12 +416,14 @@ TEST_CASE("[Keyswitches] sw_default and playing with switches")
REQUIRE( synth.getLayerView(3)->isSwitchedOn() );
synth.noteOn(0, 41, 64);
synth.noteOff(0, 41, 0);
synth.renderBlock(buffer);
REQUIRE( synth.getLayerView(0)->isSwitchedOn() );
REQUIRE( !synth.getLayerView(1)->isSwitchedOn() );
REQUIRE( synth.getLayerView(2)->isSwitchedOn() );
REQUIRE( !synth.getLayerView(3)->isSwitchedOn() );
synth.noteOn(0, 40, 64);
synth.noteOff(0, 40, 64);
synth.renderBlock(buffer);
REQUIRE( !synth.getLayerView(0)->isSwitchedOn() );
REQUIRE( synth.getLayerView(1)->isSwitchedOn() );
REQUIRE( !synth.getLayerView(2)->isSwitchedOn() );
@ -389,6 +433,7 @@ TEST_CASE("[Keyswitches] sw_default and playing with switches")
TEST_CASE("[Keyswitches] sw_previous in range")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<region> sample=*saw sw_previous=60 lokey=50 hikey=70
)");
@ -397,17 +442,22 @@ TEST_CASE("[Keyswitches] sw_previous in range")
// the test assumes that sw_previous regions are disabled by default
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 2);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
}
@ -416,28 +466,35 @@ TEST_CASE("[Keyswitches] sw_previous out of range")
{
// The behavior is the same in this case, regardless of the keyrange
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<region> sample=*saw sw_previous=60 lokey=50 hikey=55
)");
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 0);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 61, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
}
TEST_CASE("[Keyswitches] sw_lolast and sw_hilast")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<region> sw_lolast=57 sw_hilast=59 key=70 sample=*saw
<region> sw_lolast=60 sw_hilast=62 key=72 sample=*sine
@ -445,24 +502,31 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast")
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 51, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 57, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 58, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 61, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 59, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 62, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getLayerView(1)->isSwitchedOn());
}
@ -470,6 +534,7 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast")
TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_last")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<region> sw_last=40 sw_lolast=57 sw_hilast=59 key=70 sample=*saw
<region> sw_lolast=60 sw_hilast=62 sw_last=41 key=72 sample=*sine
@ -477,21 +542,27 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_last")
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 40, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 41, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 57, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 41, 64);
synth.renderBlock(buffer);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 60, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 40, 64);
synth.renderBlock(buffer);
REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getLayerView(1)->isSwitchedOn());
}
@ -499,6 +570,7 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_last")
TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_default")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<global> sw_default=58
<region> sw_lolast=57 sw_hilast=59 key=70 sample=*saw
@ -511,6 +583,7 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_default")
TEST_CASE("[Keyswitches] Multiple sw_default")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<global> sw_default=60
<region> sw_last=60 key=70 sample=*saw
@ -528,6 +601,7 @@ TEST_CASE("[Keyswitches] Multiple sw_default")
TEST_CASE("[Keyswitches] Multiple sw_default, in region")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<global> sw_default=60
<region> sw_last=58 key=70 sample=*saw

View file

@ -227,6 +227,7 @@ TEST_CASE("Legato triggers", "Region triggers")
TEST_CASE("[Triggers] sw_vel, basic")
{
Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
@ -236,6 +237,7 @@ TEST_CASE("[Triggers] sw_vel, basic")
)");
synth.noteOn(0, 60, 127);
synth.noteOn(10, 62, 10);
synth.renderBlock(buffer);
synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr);
synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr);
synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr);
@ -250,6 +252,7 @@ TEST_CASE("[Triggers] sw_vel, basic")
TEST_CASE("[Triggers] sw_vel, without sw_previous")
{
Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
@ -259,6 +262,7 @@ TEST_CASE("[Triggers] sw_vel, without sw_previous")
)");
synth.noteOn(0, 60, 127);
synth.noteOn(10, 62, 10);
synth.renderBlock(buffer);
synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr);
synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr);
synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr);
@ -273,6 +277,7 @@ TEST_CASE("[Triggers] sw_vel, without sw_previous")
TEST_CASE("[Triggers] sw_vel, with a note in between")
{
Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
@ -284,6 +289,7 @@ TEST_CASE("[Triggers] sw_vel, with a note in between")
synth.noteOn(0, 60, 127);
synth.noteOn(5, 64, 63);
synth.noteOn(10, 62, 10);
synth.renderBlock(buffer);
synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr);
synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr);
synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr);
@ -300,6 +306,7 @@ TEST_CASE("[Triggers] sw_vel, with a note in between")
TEST_CASE("[Triggers] sw_vel, with a note in between and sw_previous")
{
Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
@ -311,6 +318,7 @@ TEST_CASE("[Triggers] sw_vel, with a note in between and sw_previous")
synth.noteOn(0, 60, 127);
synth.noteOn(5, 64, 63);
synth.noteOn(10, 62, 10);
synth.renderBlock(buffer);
synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr);
synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr);
synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr);
@ -325,6 +333,7 @@ TEST_CASE("[Triggers] sw_vel, with a note in between and sw_previous")
TEST_CASE("[Triggers] sw_vel, consider the previous velocity for triggers")
{
Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
@ -336,6 +345,7 @@ TEST_CASE("[Triggers] sw_vel, consider the previous velocity for triggers")
SECTION("Should trigger") {
synth.noteOn(0, 60, 127);
synth.noteOn(10, 62, 10);
synth.renderBlock(buffer);
synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr);
synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr);
synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr);
@ -350,6 +360,7 @@ TEST_CASE("[Triggers] sw_vel, consider the previous velocity for triggers")
SECTION("Should not trigger") {
synth.noteOn(0, 60, 10);
synth.noteOn(10, 62, 127);
synth.renderBlock(buffer);
synth.dispatchMessage(client, 0, "/num_active_voices", "", nullptr);
synth.dispatchMessage(client, 0, "/voice0/trigger_value", "", nullptr);
synth.dispatchMessage(client, 0, "/voice1/trigger_value", "", nullptr);

File diff suppressed because it is too large Load diff

View file

@ -81,6 +81,55 @@ unsigned numPlayingVoices(const sfz::Synth& synth)
});
}
const std::vector<std::string> playingSamples(const sfz::Synth& synth)
{
std::vector<std::string> samples;
for (int i = 0; i < synth.getNumVoices(); ++i) {
const auto* voice = synth.getVoiceView(i);
if (!voice->releasedOrFree()) {
if (auto region = voice->getRegion())
samples.push_back(region->sampleId->filename());
}
}
return samples;
}
const std::vector<float> playingVelocities(const sfz::Synth& synth)
{
std::vector<float> velocities;
for (int i = 0; i < synth.getNumVoices(); ++i) {
const auto* voice = synth.getVoiceView(i);
if (!voice->releasedOrFree())
velocities.push_back(voice->getTriggerEvent().value);
}
return velocities;
}
const std::vector<std::string> activeSamples(const sfz::Synth& synth)
{
std::vector<std::string> samples;
for (int i = 0; i < synth.getNumVoices(); ++i) {
const auto* voice = synth.getVoiceView(i);
if (!voice->isFree()) {
const sfz::Region* region = voice->getRegion();
if (region)
samples.push_back(region->sampleId->filename());
}
}
return samples;
}
const std::vector<float> activeVelocities(const sfz::Synth& synth)
{
std::vector<float> velocities;
for (int i = 0; i < synth.getNumVoices(); ++i) {
const auto* voice = synth.getVoiceView(i);
if (!voice->isFree())
velocities.push_back(voice->getTriggerEvent().value);
}
return velocities;
}
std::string createDefaultGraph(std::vector<std::string> lines, int numRegions)
{
for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) {

View file

@ -78,6 +78,38 @@ const std::vector<const sfz::Voice*> getPlayingVoices(const sfz::Synth& synth);
*/
unsigned numPlayingVoices(const sfz::Synth& synth);
/**
* @brief Get the playing samples
*
* @param synth
* @return unsigned
*/
const std::vector<std::string> playingSamples(const sfz::Synth& synth);
/**
* @brief Get the playing notes velocities
*
* @param synth
* @return unsigned
*/
const std::vector<float> playingVelocities(const sfz::Synth& synth);
/**
* @brief Get the active samples
*
* @param synth
* @return unsigned
*/
const std::vector<std::string> activeSamples(const sfz::Synth& synth);
/**
* @brief Get the active notes velocities
*
* @param synth
* @return unsigned
*/
const std::vector<float> activeVelocities(const sfz::Synth& synth);
/**
* @brief Create the default dot graph representation for standard regions
*