From bd5f491fb317e66ae0c5aeb699c7d41b8d623843 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 30 Nov 2019 15:40:13 +0100 Subject: [PATCH] Added tests for the dynamic voice settings --- src/sfizz/Synth.cpp | 17 +++-- src/sfizz/Synth.h | 150 +++++++++++++++++++++++-------------------- src/sfizz/Voice.h | 12 ++++ tests/CMakeLists.txt | 1 + tests/SynthT.cpp | 97 ++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 76 deletions(-) create mode 100644 tests/SynthT.cpp diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 221a499d..8d8cb447 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -115,7 +115,7 @@ void sfz::Synth::clear() while (inCallback) { std::this_thread::sleep_for(1ms); } - + for (auto &voice: voices) voice->reset(); for (auto& list: noteActivationLists) @@ -337,7 +337,7 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept while (inCallback) { std::this_thread::sleep_for(1ms); } - + this->sampleRate = sampleRate; for (auto& voice : voices) voice->setSampleRate(sampleRate); @@ -347,7 +347,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept { ScopedFTZ ftz; buffer.fill(0.0f); - + AtomicGuard callbackGuard { inCallback }; if (!canEnterCallback) return; @@ -486,10 +486,17 @@ int sfz::Synth::getNumCurves() const noexcept { return numCurves; } + const sfz::Region* sfz::Synth::getRegionView(int idx) const noexcept { return (size_t)idx < regions.size() ? regions[idx].get() : nullptr; } + +const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept +{ + return (size_t)idx < voices.size() ? voices[idx].get() : nullptr; +} + std::set sfz::Synth::getUnknownOpcodes() const noexcept { return unknownOpcodes; @@ -534,7 +541,7 @@ void sfz::Synth::resetVoices(int numVoices) voice->setSampleRate(this->sampleRate); voice->setSamplesPerBlock(this->samplesPerBlock); } - + voiceViewArray.reserve(numVoices); this->numVoices = numVoices; -} \ No newline at end of file +} diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 546df2d0..35280db0 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -40,35 +40,35 @@ namespace sfz { * @brief This class is the core of the sfizz library. In C++ it is the main point * of entry and in C the interface basically maps the functions of the class into * C bindings. - * + * * The JACK client provides an example of how you can use this class as an entry * point for your own projects. Just include this header and compile against the * static library. If you wish to use the shared library you should rather use the * C bindings. - * + * * This class derives from the Parser and provides a specific set of callbacks; see * the Parser documentation for more precisions. - * + * * The Synth object contains: * - A set of SFZ Regions that get filled up upon parsing * - A set of Voices that play the sounds of the regions when triggered. * - Some singleton resources, particularly the midiState which contains the current * midi status (note is on or off, last note velocity, current CC values, ...) * as well as a FilePool that preloads and give access to files. - * + * * The synth is callback based, in the sense that it renders audio block by block - * using the renderBlock() function. Between each call to renderBlock() you have to + * using the renderBlock() function. Between each call to renderBlock() you have to * send the relevent events for the block in the form of MIDI events: noteOn(), * noteOff(), cc(). You can also send pitchBend(), aftertouch() and bpm() * events -- but as of 2019 they are not handled. - * + * * All events have a delay information, which must be less than the size of the * next call to renderBlock() in units of frames or samples. For example, if you - * will call to render a block of 256 samples, all the events you send to the + * will call to render a block of 256 samples, all the events you send to the * synth should have a delay parameter strictly lower than 256. Events beyond 256 * may be completely ignored by the synth as the incoming event buffer is cleared * during the renderBlock() call. - * + * * The jack_client.cpp file contains examples of the most classical usage of the * synth and can be used as a reference. */ @@ -76,110 +76,118 @@ class Synth : public Parser { public: /** * @brief Construct a new Synth object with no voices. If you want sound - * you will need to call setNumVoices() before playing. - * + * you will need to call setNumVoices() before playing. + * */ Synth(); /** * @brief Construct a new Synth object with a specified number of voices. - * - * @param numVoices + * + * @param numVoices */ Synth(int numVoices); /** * @brief Empties the current regions and load a new SFZ file into the synth. - * - * This function will disable all callbacks so it is safe to call from a + * + * This function will disable all callbacks so it is safe to call from a * UI thread for example, although it may generate a click. However it is * not reentrant, so you should not call it from concurrent threads. - * - * @param file - * @return true + * + * @param file + * @return true * @return false if the file was not found or no regions were loaded. */ bool loadSfzFile(const fs::path& file) final; /** * @brief Get the current number of regions loaded - * - * @return int + * + * @return int */ int getNumRegions() const noexcept; /** * @brief Get the current number of groups loaded - * - * @return int + * + * @return int */ int getNumGroups() const noexcept; /** * @brief Get the current number of masters loaded - * - * @return int + * + * @return int */ int getNumMasters() const noexcept; /** * @brief Get the current number of curves loaded - * - * @return int + * + * @return int */ int getNumCurves() const noexcept; /** * @brief Get a raw view into a specific region. This is mostly used * for testing. - * - * @param idx - * @return const Region* + * + * @param idx + * @return const Region* */ const Region* getRegionView(int idx) const noexcept; + /** + * @brief Get a raw view into a specific voice. This is mostly used + * for testing. + * + * @param idx + * @return const Region* + */ + const Voice* getVoiceView(int idx) const noexcept; /** * @brief Get a list of unknown opcodes. The lifetime of the * string views in the code are linked to the currently loaded * sfz file. - * + * * TODO: change this to strings we don't really care about performance * here and this hurts the C interface. - * - * @return std::set + * + * @return std::set */ std::set getUnknownOpcodes() const noexcept; /** * @brief Get the number of preloaded samples in the synth - * - * @return size_t + * + * @return size_t */ size_t getNumPreloadedSamples() const noexcept; /** * @brief Set the maximum size of the blocks for the callback. The actual - * size can be lower in each callback but should not be larger + * size can be lower in each callback but should not be larger * than this value. - * - * @param samplesPerBlock + * + * @param samplesPerBlock */ void setSamplesPerBlock(int samplesPerBlock) noexcept; /** - * @brief Set the sample rate. If you do not call it it is initialized + * @brief Set the sample rate. If you do not call it it is initialized * to sfz::config::defaultSampleRate. - * - * @param sampleRate + * + * @param sampleRate */ void setSampleRate(float sampleRate) noexcept; /** * @brief Get the current value for the volume, in dB. - * - * @return float + * + * @return float */ float getVolume() const noexcept; /** * @brief Set the value for the volume. This value will be * clamped within sfz::default::volumeRange. - * - * @param volume + * + * @param volume */ - void setVolume(float volume) noexcept; + void setVolume(float volume) noexcept; /** * @brief Send a note on 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(). * @param channel the midi channel for the event @@ -189,7 +197,7 @@ public: void noteOn(int delay, int channel, int noteNumber, uint8_t velocity) noexcept; /** * @brief Send a note off 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(). * @param channel the midi channel for the event @@ -199,7 +207,7 @@ public: void noteOff(int delay, int channel, int noteNumber, uint8_t velocity) noexcept; /** * @brief Send a CC 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(). * @param channel the midi channel for the event @@ -209,7 +217,7 @@ public: void cc(int delay, int channel, int ccNumber, uint8_t ccValue) noexcept; /** * @brief Send a pitch bend 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(). * @param channel the midi channel for the event @@ -218,7 +226,7 @@ public: void pitchWheel(int delay, int channel, int pitch) noexcept; /** * @brief Send a aftertouch 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(). * @param channel the midi channel for the event @@ -227,7 +235,7 @@ public: void aftertouch(int delay, int channel, uint8_t aftertouch) noexcept; /** * @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(). * @param channel the midi channel for the event @@ -239,36 +247,36 @@ public: * in its waiting state for the next batch of events. The size of the block is integrated * in the AudioSpan object. You can build an AudioSpan implicitely from a large number * of source objects; check the AudioSpan reference for more precision. - * + * * @param buffer the buffer to write the next block into; this should be a stereo buffer. */ void renderBlock(AudioSpan buffer) noexcept; /** * @brief Get the number of active voices - * - * @return int + * + * @return int */ int getNumActiveVoices() const noexcept; /** * @brief Get the total number of voices in the synth (the polyphony) - * - * @return int + * + * @return int */ int getNumVoices() const noexcept; /** * @brief Change the number of voices (the polyphony) - * - * @param numVoices + * + * @param numVoices */ void setNumVoices(int numVoices) noexcept; /** - * @brief Trigger a garbage collection, which removes the samples that are + * @brief Trigger a garbage collection, which removes the samples that are * loaded by the FilePool after being requested by the voices. This does * not concern the preloaded samples, only the samples loaded to be played - * fully. This function is run regularly in a background thread so normally + * fully. This function is run regularly in a background thread so normally * you should not need to call it explicitely. - * + * */ void garbageCollect() noexcept; protected: @@ -276,7 +284,7 @@ protected: * @brief The parser callback; this is called by the parent object each time * a new region, group, master, global, curve or control set of opcodes * appears in the parser - * + * * @param header the header for the set of opcodes * @param members the opcode members */ @@ -292,25 +300,25 @@ private: /** * @brief Remove all regions, resets all voices and clears everything * to bring back the synth in its original state. - * + * */ void clear(); /** * @brief Resets and possibly changes the number of voices (polyphony) in * the synth. - * - * @param numVoices + * + * @param numVoices */ void resetVoices(int numVoices); /** * @brief Helper function to dispatch opcodes - * + * * @param members the opcodes of the block */ void handleGlobalOpcodes(const std::vector& members); /** * @brief Helper function to dispatch opcodes - * + * * @param members the opcodes of the block */ void handleControlOpcodes(const std::vector& members); @@ -318,11 +326,11 @@ private: * @brief Helper function to merge all the currently active opcodes * as set by the successive callbacks and create a new region to store * in the synth. - * + * * @param regionOpcodes the opcodes that are specific to the region */ void buildRegion(const std::vector& regionOpcodes); - + // Opcode memory; these are used to build regions, as a new region // will integrate opcodes from the group, master and global block std::vector globalOpcodes; @@ -336,8 +344,8 @@ private: /** * @brief Find a voice that is not currently playing - * - * @return Voice* + * + * @return Voice* */ Voice* findFreeVoice() noexcept; // Names for the cc as set by the cc_label or cc_name opcodes @@ -374,4 +382,4 @@ private: LEAK_DETECTOR(Synth); }; -} \ No newline at end of file +} diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 59fd94ab..350265c2 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -73,6 +73,18 @@ public: * @param samplesPerBlock */ void setSamplesPerBlock(int samplesPerBlock) noexcept; + /** + * @brief Get the sample rate of the voice. + * + * @return float + */ + float getSampleRate() const noexcept { return sampleRate; } + /** + * @brief Get the expected block size. + * + * @return int + */ + int getSamplesPerBlock() const noexcept { return samplesPerBlock; } /** * @brief Start playing a region after a short delay for different triggers (note on, off, cc) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9270653d..dfdd4255 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,6 +20,7 @@ set(SFIZZ_TEST_SOURCES ADSREnvelopeT.cpp LinearEnvelopeT.cpp MainT.cpp + SynthT.cpp RegionTriggersT.cpp ) diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp new file mode 100644 index 00000000..33c2ba13 --- /dev/null +++ b/tests/SynthT.cpp @@ -0,0 +1,97 @@ +// Copyright (c) 2019, Paul Ferrand +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: + +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "sfizz.hpp" +#include "catch2/catch.hpp" +using namespace Catch::literals; + +constexpr int blockSize { 256 }; + +TEST_CASE("[Synth] Play and check active voices") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(256); + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz"); + + synth.noteOn(0, 1, 36, 24); + synth.noteOn(0, 1, 36, 89); + REQUIRE(synth.getNumActiveVoices() == 2); + // Render for a while + for (int i = 0; i < 200; ++i) + synth.renderBlock(buffer); + REQUIRE(synth.getNumActiveVoices() == 0); +} + +TEST_CASE("[Synth] Change the number of voice while playing") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(256); + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz"); + + synth.noteOn(0, 1, 36, 24); + synth.noteOn(0, 1, 36, 89); + synth.renderBlock(buffer); + REQUIRE(synth.getNumActiveVoices() == 2); + synth.setNumVoices(8); + REQUIRE(synth.getNumActiveVoices() == 0); + REQUIRE(synth.getNumVoices() == 8); +} + +TEST_CASE("[Synth] Check that the sample per block and sample rate are actually propagated to all voices even on recreation") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(256); + synth.setSampleRate(96000); + for (int i = 0; i < synth.getNumVoices(); ++i) + { + REQUIRE( synth.getVoiceView(i)->getSamplesPerBlock() == 256 ); + REQUIRE( synth.getVoiceView(i)->getSampleRate() == 96000.0f ); + } + synth.setNumVoices(8); + for (int i = 0; i < synth.getNumVoices(); ++i) + { + REQUIRE( synth.getVoiceView(i)->getSamplesPerBlock() == 256 ); + REQUIRE( synth.getVoiceView(i)->getSampleRate() == 96000.0f ); + } + synth.setSamplesPerBlock(128); + synth.setSampleRate(48000); + for (int i = 0; i < synth.getNumVoices(); ++i) + { + REQUIRE( synth.getVoiceView(i)->getSamplesPerBlock() == 128 ); + REQUIRE( synth.getVoiceView(i)->getSampleRate() == 48000.0f ); + } + synth.setNumVoices(64); + for (int i = 0; i < synth.getNumVoices(); ++i) + { + REQUIRE( synth.getVoiceView(i)->getSamplesPerBlock() == 128 ); + REQUIRE( synth.getVoiceView(i)->getSampleRate() == 48000.0f ); + } +} + +TEST_CASE("[Synth] Check that the sample rate and sample per block is changed when changing the number of voices") +{ + sfz::Synth synth; + +}