diff --git a/sfizz/AtomicGuard.h b/sfizz/AtomicGuard.h index c36f1bd4..874e1f61 100644 --- a/sfizz/AtomicGuard.h +++ b/sfizz/AtomicGuard.h @@ -21,10 +21,63 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +/** + * @brief This file contains a pair of RAII helpers that handle some form + * of lock-free mutex-type protection adapter to audio applications where you have 1 priority thread + * that should never block and would rather return silence than wait, and another low-priority + * thread that handles long computations. + * + * @code{.cpp} + * + * // Somewhere in a class... + * std::atomic canEnterCallback; + * std::atomic inCallback; + * + * void functionThatSuspendsCallback() + * { + * AtomicDisabler callbackDisabler { canEnterCallback }; + * + * while (inCallback) { + * std::this_thread::sleep_for(1ms); + * } + * + * // Do your thing. + * } + * + * void callback(int samplesPerBlock) noexcept + * { + * AtomicGuard callbackGuard { inCallback }; + * if (!canEnterCallback) + * return; + * + * // Do your thing. + * } + * @endcode + * There are probably many ways to improve these and probably even debug them. + * The spinlocking itself could be integrated in the constructor, although the + * check for return in the callback could not. + */ #include namespace sfz { +/** + * @brief Simple class to set an atomic to true and automatically set it back to false on + * destruction. + * + * You call it like this assuming you need indicate that you are in e.g. a callback + * @code{.cpp} + * void functionToProtect() + * { + * AtomicGuard { guard }; + * + * // Do stuff, the atomic will be set back to false as soon as you're back + * } + * @endcode + * Note that this is not thread-safe at all, in the sense that it is only meant to be + * used with 2 threads along with the AtomicDisabler. One thread uses AtomicGuards, the other + * AtomicDisablers, and no other contending thread can share this pair of atomics. + */ class AtomicGuard { public: @@ -42,6 +95,23 @@ private: std::atomic& guard; }; +/** + * @brief Simple class to set an atomic to false and automatically set it back to true on + * destruction. + * + * You call it like this assuming you need to disable e.g. a callback + * @code{.cpp} + * void functionThatDisableAnotherFunction() + * { + * AtomicDisabler { disabler }; + * + * // Do stuff, the atomic will be set back to true as soon as you're back + * } + * @endcode + * Note that this is not thread-safe at all, in the sense that it is only meant to be + * used with 2 threads along with the AtomicGuard. One thread uses AtomicGuards, the other + * AtomicDisabler, and no other contending thread can share this pair of atomics. + */ class AtomicDisabler { public: diff --git a/sfizz/AudioBuffer.h b/sfizz/AudioBuffer.h index 2ef1b35a..98105a5b 100644 --- a/sfizz/AudioBuffer.h +++ b/sfizz/AudioBuffer.h @@ -32,7 +32,16 @@ namespace sfz { - +/** + * @brief A class to handle a collection of buffers, where each buffer has the same size. + * + * Unlike AudioSpan, this class *owns* its underlying buffers and they are freed when the buffer + * is destroyed. + * + * @tparam Type the underlying type of the buffers + * @tparam MaxChannels the maximum number of channels in the buffer + * @tparam Alignment the alignment for the buffers + */ template class AudioBuffer { public: @@ -43,9 +52,21 @@ public: using const_iterator = const_pointer; using size_type = size_t; + /** + * @brief Construct a new Audio Buffer object + * + */ AudioBuffer() { } + + /** + * @brief Construct a new Audio Buffer object with a specified number of + * channels and frames. + * + * @param numChannels + * @param numFrames + */ AudioBuffer(int numChannels, int numFrames) : numChannels(numChannels) , numFrames(numFrames) @@ -54,6 +75,13 @@ public: buffers[i] = std::make_unique(numFrames); } + /** + * @brief Resizes all the underlying buffers to a new size. + * + * @param newSize + * @return true if the resize worked + * @return false otherwise + */ bool resize(size_type newSize) { bool returnedOK = true; @@ -62,6 +90,12 @@ public: return returnedOK; } + /** + * @brief Return an iterator to a specific channel with a non-const type. + * + * @param channelIndex + * @return iterator + */ iterator channelWriter(int channelIndex) { ASSERT(channelIndex < numChannels) @@ -71,6 +105,12 @@ public: return {}; } + /** + * @brief Returns a sentinel for the channelWriter(channelIndex) iterator + * + * @param channelIndex + * @return iterator + */ iterator channelWriterEnd(int channelIndex) { ASSERT(channelIndex < numChannels) @@ -80,6 +120,12 @@ public: return {}; } + /** + * @brief Returns a const iterator for a specific channel + * + * @param channelIndex + * @return const_iterator + */ const_iterator channelReader(int channelIndex) const { ASSERT(channelIndex < numChannels) @@ -89,6 +135,12 @@ public: return {}; } + /** + * @brief Returns a sentinel for the channelReader(channelIndex) iterator + * + * @param channelIndex + * @return const_iterator + */ const_iterator channelReaderEnd(int channelIndex) const { ASSERT(channelIndex < numChannels) @@ -98,6 +150,12 @@ public: return {}; } + /** + * @brief Get a Span for a specific channel + * + * @param channelIndex + * @return absl::Span + */ absl::Span getSpan(int channelIndex) const { ASSERT(channelIndex < numChannels) @@ -107,32 +165,67 @@ public: return {}; } + /** + * @brief Get a const Span object for a specific channel + * + * @param channelIndex + * @return absl::Span + */ absl::Span getConstSpan(int channelIndex) const { return getSpan(channelIndex); } + /** + * @brief Add a channel to the buffer with the current number of frames. + * + */ void addChannel() { if (numChannels < MaxChannels) buffers[numChannels++] = std::make_unique(numFrames); } + /** + * @brief Get the number of elements in each buffer + * + * @return size_type + */ size_type getNumFrames() const { return numFrames; } + /** + * @brief Get the number of channels + * + * @return int + */ int getNumChannels() const { return numChannels; } + /** + * @brief Check if the buffers contains no elements + * + * @return true + * @return false + */ bool empty() const { return numFrames == 0; } + /** + * @brief Get a reference to a given element in a given buffer. + * + * In release builds this is not checked and may touch bad memory. + * + * @param channelIndex + * @param frameIndex + * @return Type& + */ Type& getSample(int channelIndex, size_type frameIndex) { // Uhoh @@ -142,6 +235,13 @@ public: return *(buffers[channelIndex]->data() + frameIndex); } + /** + * @brief Alias for getSample(...) + * + * @param channelIndex + * @param frameIndex + * @return Type& + */ Type& operator()(int channelIndex, size_type frameIndex) { return getSample(channelIndex, frameIndex); diff --git a/sfizz/MathHelpers.h b/sfizz/MathHelpers.h index 86ac7c09..601152e7 100644 --- a/sfizz/MathHelpers.h +++ b/sfizz/MathHelpers.h @@ -21,6 +21,13 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +/** + * @file MathHelpers.h + * @author Paul Ferrand (paul@ferrand.cc) + * @brief Contains math helper functions and math constants + * @version 0.1 + * @date 2019-11-23 + */ #pragma once #include #include @@ -34,43 +41,91 @@ inline constexpr T min(T op1, T op2, T op3) { return std::min(op1, std::min(op2, template inline constexpr T min(T op1, T op2, T op3, T op4) { return std::min(op1, std::min(op2, std::min(op3, op4))); } - +/** + * @brief Converts db values into power (applies 10**(in/10)) + * + * @tparam Type + * @param in + * @return Type + */ template inline constexpr Type db2pow(Type in) { return std::pow(static_cast(10.0), in * static_cast(0.1)); } +/** + * @brief Converts power values into dB (applies 10log10(in)) + * + * @tparam Type + * @param in + * @return Type + */ template inline constexpr Type pow2db(Type in) { return static_cast(10.0) * std::log10(in); } +/** + * @brief Converts dB values to magnitude (applies 10**(in/20)) + * + * @tparam Type + * @param in + * @return constexpr Type + */ template inline constexpr Type db2mag(Type in) { return std::pow(static_cast(10.0), in * static_cast(0.05)); } +/** + * @brief Converts magnitude values into dB (applies 20log10(in)) + * + * @tparam Type + * @param in + * @return Type + */ template inline constexpr Type mag2db(Type in) { return static_cast(20.0) * std::log10(in); } +/** + * @brief Global random singletons + * + * TODO: could be moved into a singleton class holder + * + */ namespace Random { static std::random_device randomDevice; static std::mt19937 randomGenerator { randomDevice() }; } // namespace Random +/** + * @brief Converts a midi note to a frequency value + * + * @param noteNumber + * @return float + */ inline float midiNoteFrequency(const int noteNumber) { return 440.0f * std::pow(2.0f, (noteNumber - 69) / 12.0f); } +/** + * @brief Clamps a value between bounds, including the bounds! + * + * @tparam T + * @param v + * @param lo + * @param hi + * @return T + */ template -constexpr const T& clamp( const T& v, const T& lo, const T& hi ) +constexpr T clamp( const T& v, const T& lo, const T& hi ) { assert( !(hi < lo) ); return (v < lo) ? lo : (hi < v) ? hi : v; diff --git a/sfizz/Range.h b/sfizz/Range.h index 5ea5b10e..78e3151c 100644 --- a/sfizz/Range.h +++ b/sfizz/Range.h @@ -26,7 +26,6 @@ #include #include - namespace sfz { diff --git a/sfizz/SIMDHelpers.h b/sfizz/SIMDHelpers.h index 84c7ceff..dcb21eb6 100644 --- a/sfizz/SIMDHelpers.h +++ b/sfizz/SIMDHelpers.h @@ -21,6 +21,39 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +/** + * @file SIMDHelpers.h + * @author Paul Ferrand (paul@ferrand.cc) + * @brief This file contains useful functions to treat buffers of numerical values + * (e.g. a buffer of floats usually). + * + * These functions are templated to apply on + * various underlying buffer types, and this file contains the generic version of the + * function. Some templates specializations exists for different architecture that try + * to make use of SIMD intrinsics; you can find such a file in SIMDSSE.cpp and possibly + * someday SIMDNEON.cpp for ARM platforms. + * + * If you want to write specializations for float buffers the idea is to start from the SIMDDummy + * file that just calls back the generic implementation, and implement the specializations you + * wish from this list. You can then either activate or deactivate a SIMD version by default + * using the variables in Config.h, or call e.g. writeInterleaved(...) to use the + * SIMD version of writeInterleaved. To implement e.g. double template specializations you + * will need to amend this file to pre-declare the specializations, and create a file similar to + * SIMDxxx.cpp. + * + * All the SIMD functions are benchmarked. If you run the benchmark for a given function you can check + * if it is interesting to run the SIMD version by default. The interest is that you can activate + * and deactivate each SIMD specialization with a fine granularity, since SIMD performance + * will be very dependent on the processor architecture. Modern processors can also organize their + * instructions so that scalar non-SIMD code runs sometimes much more efficiently than SIMD code + * especially when the latter does not operate on misaligned buffers. + * + * @version 0.1 + * @date 2019-11-23 + * + * @copyright Copyright (c) 2019 + * + */ #pragma once #include "Config.h" #include "Debug.h" @@ -39,6 +72,17 @@ namespace _internals { } } +/** + * @brief Read interleaved stereo data from a buffer and separate it in a left/right pair of buffers. + * + * The output size will be the minimum of the input span and output spans size. + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param input + * @param outputLeft + * @param outputRight + */ template void readInterleaved(absl::Span input, absl::Span outputLeft, absl::Span outputRight) noexcept { @@ -62,6 +106,17 @@ namespace _internals { } } +/** + * @brief Write a pair of left and right stereo input into a single buffer interleaved. + * + * The output size will be the minimum of the input spans and output span size. + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param inputLeft + * @param inputRight + * @param output + */ template void writeInterleaved(absl::Span inputLeft, absl::Span inputRight, absl::Span output) noexcept { @@ -81,6 +136,14 @@ void writeInterleaved(absl::Span inputLeft, absl::Span template <> void readInterleaved(absl::Span input, absl::Span outputLeft, absl::Span outputRight) noexcept; +/** + * @brief Fill a buffer with a value; comparable to std::fill in essence. + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param output + * @param value + */ template void fill(absl::Span output, T value) noexcept { @@ -90,6 +153,14 @@ void fill(absl::Span output, T value) noexcept template <> void fill(absl::Span output, float value) noexcept; +/** + * @brief Exp math function + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param input + * @param output + */ template void exp(absl::Span input, absl::Span output) noexcept { @@ -102,6 +173,16 @@ void exp(absl::Span input, absl::Span output) noexcept template <> void exp(absl::Span input, absl::Span output) noexcept; +/** + * @brief Log math function + * + * The output size will be the minimum of the input span and output span size. + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param input + * @param output + */ template void log(absl::Span input, absl::Span output) noexcept { @@ -114,6 +195,16 @@ void log(absl::Span input, absl::Span output) noexcept template <> void log(absl::Span input, absl::Span output) noexcept; +/** + * @brief sin math function + * + * The output size will be the minimum of the input span and output span size. + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param input + * @param output + */ template void sin(absl::Span input, absl::Span output) noexcept { @@ -126,6 +217,16 @@ void sin(absl::Span input, absl::Span output) noexcept template <> void sin(absl::Span input, absl::Span output) noexcept; +/** + * @brief cos math function + * + * The output size will be the minimum of the input span and output span size. + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param input + * @param output + */ template void cos(absl::Span input, absl::Span output) noexcept { @@ -160,6 +261,25 @@ namespace _internals { } } +/** + * @brief Computes an integer index and 2 float coefficients corresponding to the + * linear interpolation procedure. This version will saturate the index to the upper + * bound if the upper bound is reached. + * + * The indices are computed starting from the given floatIndex, and each increment + * is given by the elements of jumps. + * The output size will be the minimum of the inputs span and outputs span size. + * + * @tparam T the underlying type + * @tparam SIMD use the SIMD version or the scalar version + * @param jumps the floating point increments to the index + * @param leftCoeffs the linear interpolation coefficients for the left value + * @param rightCoeffs the linear interpolation coefficients for the right value + * @param indices the integer sample indices for the left values; the right values for interpolation at index i are (indices[i] + 1) and not indices[i+1] + * @param floatIndex the starting floating point index + * @param loopEnd the end of the "loop" which is not really a loop because it saturate. + * @return float + */ template float saturatingSFZIndex(absl::Span jumps, absl::Span leftCoeffs, absl::Span rightCoeffs, absl::Span indices, T floatIndex, T loopEnd) noexcept { diff --git a/sfizz/ScopedFTZ.h b/sfizz/ScopedFTZ.h index 4590c540..1db03bbc 100644 --- a/sfizz/ScopedFTZ.h +++ b/sfizz/ScopedFTZ.h @@ -21,6 +21,10 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +/** + * @brief Flush floating points to zero and disable denormals as an RAII helper. + * + */ class ScopedFTZ { public: diff --git a/sfizz/SfzHelpers.h b/sfizz/SfzHelpers.h index 87df290f..74a528f6 100644 --- a/sfizz/SfzHelpers.h +++ b/sfizz/SfzHelpers.h @@ -35,12 +35,27 @@ using CCValueArray = std::array; using CCValuePair = std::pair ; using CCNamePair = std::pair; +/** + * @brief Converts cents to a pitch ratio + * + * @tparam T + * @param cents + * @param centsPerOctave + * @return constexpr float + */ template inline constexpr float centsFactor(T cents, T centsPerOctave = 1200) { return std::pow(2.0f, static_cast(cents) / centsPerOctave); } +/** + * @brief Normalize a CC value between (T)0.0 and (T)1.0 + * + * @tparam T + * @param ccValue + * @return constexpr float + */ template inline constexpr float normalizeCC(T ccValue) { @@ -48,18 +63,40 @@ inline constexpr float normalizeCC(T ccValue) return static_cast(std::min(std::max(ccValue, static_cast(0)), static_cast(127))) / 127.0f; } +/** + * @brief Normalize a percentage between 0 and 1 + * + * @tparam T + * @param percentValue + * @return constexpr float + */ template inline constexpr float normalizePercents(T percentValue) { return std::min(std::max(static_cast(percentValue), 0.0f), 100.0f) / 100.0f; } +/** + * @brief Normalize a possibly negative percentage between -1 and 1 + * + * @tparam T + * @param percentValue + * @return constexpr float + */ template inline constexpr float normalizeNegativePercents(T percentValue) { return std::min(std::max(static_cast(percentValue), -100.0f), 100.0f) / 100.0f; } +/** + * @brief If a cc switch exists for the value, returns the value with the CC modifier, otherwise returns the value alone. + * + * @param ccValues + * @param ccSwitch + * @param value + * @return float + */ inline float ccSwitchedValue(const CCValueArray& ccValues, const absl::optional& ccSwitch, float value) noexcept { if (ccSwitch) @@ -68,6 +105,12 @@ inline float ccSwitchedValue(const CCValueArray& ccValues, const absl::optional< return value; } +/** + * @brief Convert a note in string to its equivalent midi note number + * + * @param value + * @return absl::optional + */ absl::optional readNoteValue(const absl::string_view& value); } // namespace sfz diff --git a/sfizz/StringViewHelpers.h b/sfizz/StringViewHelpers.h index 4b3ad8d0..4248908e 100644 --- a/sfizz/StringViewHelpers.h +++ b/sfizz/StringViewHelpers.h @@ -21,9 +21,25 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +/** + * @file StringViewHelpers.h + * @author Paul Ferrand (paul@ferrand.cc) + * @brief Contains some helper functions for string views + * @version 0.1 + * @date 2019-11-23 + * + * @copyright Copyright (c) 2019 + * + */ + #pragma once #include "absl/strings/string_view.h" +/** + * @brief Removes the whitespace on a string_view in place + * + * @param s + */ inline void trimInPlace(absl::string_view& s) { const auto leftPosition = s.find_first_not_of(" \r\t\n\f\v"); @@ -36,22 +52,30 @@ inline void trimInPlace(absl::string_view& s) } } +/** + * @brief Removes the whitespace on a string_view and return a new string_view + * + * @param s + * @return absl::string_view + */ inline absl::string_view trim(absl::string_view s) { - const auto leftPosition = s.find_first_not_of(" \r\t\n\f\v"); - if (leftPosition != s.npos) { - s.remove_prefix(leftPosition); - const auto rightPosition = s.find_last_not_of(" \r\t\n\f\v"); - s.remove_suffix(s.size() - rightPosition - 1); - } else { - s.remove_suffix(s.size()); - } + trimInPlace(s); return s; } constexpr uint64_t Fnv1aBasis = 0x811C9DC5; constexpr uint64_t Fnv1aPrime = 0x01000193; +/** + * @brief Compile-time hashing function to be used mostly with switch/case statements. + * + * See e.g. the Region.cpp file + * + * @param s the input string to be hashed + * @param h the hashing seed to use + * @return uint64_t + */ inline constexpr uint64_t hash(absl::string_view s, uint64_t h = Fnv1aBasis) { if (s.length() > 0) diff --git a/sfizz/Synth.cpp b/sfizz/Synth.cpp index e07a6f36..221a499d 100644 --- a/sfizz/Synth.cpp +++ b/sfizz/Synth.cpp @@ -320,6 +320,7 @@ void sfz::Synth::garbageCollect() noexcept void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept { AtomicDisabler callbackDisabler { canEnterCallback }; + while (inCallback) { std::this_thread::sleep_for(1ms); } @@ -347,11 +348,10 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept ScopedFTZ ftz; buffer.fill(0.0f); + AtomicGuard callbackGuard { inCallback }; if (!canEnterCallback) return; - AtomicGuard callbackGuard { inCallback }; - auto tempSpan = AudioSpan(tempBuffer).first(buffer.getNumFrames()); for (auto& voice : voices) { voice->renderBlock(tempSpan); @@ -369,11 +369,10 @@ void sfz::Synth::noteOn(int delay, int channel, int noteNumber, uint8_t velocity midiState.noteOn(noteNumber, velocity); + AtomicGuard callbackGuard { inCallback }; if (!canEnterCallback) return; - AtomicGuard callbackGuard { inCallback }; - auto randValue = randNoteDistribution(Random::randomGenerator); for (auto& region : noteActivationLists[noteNumber]) { @@ -402,11 +401,10 @@ void sfz::Synth::noteOff(int delay, int channel, int noteNumber, uint8_t velocit ASSERT(noteNumber >= 0); // DBG("Received note " << noteNumber << "/" << +velocity << " OFF at time " << delay); + AtomicGuard callbackGuard { inCallback }; if (!canEnterCallback) return; - AtomicGuard callbackGuard { inCallback }; - // FIXME: Some keyboards (e.g. Casio PX5S) can send a real note-off velocity. In this case, do we have a // way in sfz to specify that a release trigger should NOT use the note-on velocity? // auto replacedVelocity = (velocity == 0 ? sfz::getNoteVelocity(noteNumber) : velocity); @@ -435,11 +433,10 @@ void sfz::Synth::cc(int delay, int channel, int ccNumber, uint8_t ccValue) noexc ASSERT(ccNumber < 128); ASSERT(ccNumber >= 0); + AtomicGuard callbackGuard { inCallback }; if (!canEnterCallback) return; - AtomicGuard callbackGuard { inCallback }; - for (auto& voice : voices) voice->registerCC(delay, channel, ccNumber, ccValue); diff --git a/sfizz/Synth.h b/sfizz/Synth.h index 7cffeac2..546df2d0 100644 --- a/sfizz/Synth.h +++ b/sfizz/Synth.h @@ -36,38 +36,250 @@ #include 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 + * 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 + * 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. + */ 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. + * + */ Synth(); + /** + * @brief Construct a new Synth object with a specified number of voices. + * + * @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 + * 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 + * @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 + */ int getNumRegions() const noexcept; + /** + * @brief Get the current number of groups loaded + * + * @return int + */ int getNumGroups() const noexcept; + /** + * @brief Get the current number of masters loaded + * + * @return int + */ int getNumMasters() const noexcept; + /** + * @brief Get the current number of curves loaded + * + * @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* + */ const Region* getRegionView(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 + */ std::set getUnknownOpcodes() const noexcept; + /** + * @brief Get the number of preloaded samples in the synth + * + * @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 + * than this value. + * + * @param samplesPerBlock + */ void setSamplesPerBlock(int samplesPerBlock) noexcept; + /** + * @brief Set the sample rate. If you do not call it it is initialized + * to sfz::config::defaultSampleRate. + * + * @param sampleRate + */ void setSampleRate(float sampleRate) noexcept; + /** + * @brief Get the current value for the volume, in dB. + * + * @return float + */ float getVolume() const noexcept; + /** + * @brief Set the value for the volume. This value will be + * clamped within sfz::default::volumeRange. + * + * @param volume + */ void setVolume(float volume) noexcept; - void renderBlock(AudioSpan buffer) 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 + * @param noteNumber the midi note number + * @param velocity the midi note velocity + */ 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 + * @param noteNumber the midi note number + * @param velocity the midi note velocity + */ 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 + * @param ccNumber the cc number + * @param ccValue the cc value + */ 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 + * @param pitch the pitch value + */ 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 + * @param aftertouch the aftertouch value + */ 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 + * @param secondsPerQuarter the new period of the quarter note + */ void tempo(int delay, float secondsPerQuarter) noexcept; + /** + * @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 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 + */ int getNumActiveVoices() const noexcept; + /** + * @brief Get the total number of voices in the synth (the polyphony) + * + * @return int + */ int getNumVoices() const noexcept; + /** + * @brief Change the number of voices (the polyphony) + * + * @param numVoices + */ void setNumVoices(int numVoices) noexcept; + /** + * @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 + * you should not need to call it explicitely. + * + */ void garbageCollect() noexcept; 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 + */ void callback(absl::string_view header, const std::vector& members) final; private: @@ -76,43 +288,89 @@ private: int numGroups { 0 }; int numMasters { 0 }; int numCurves { 0 }; + + /** + * @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 + */ 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); + /** + * @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; std::vector masterOpcodes; std::vector groupOpcodes; + // Singletons passed as references to the voices + // TODO: these should probably go in a global singleton holder along with a buffer distribution and LFO/EG stuff... FilePool filePool; MidiState midiState; + + /** + * @brief Find a voice that is not currently playing + * + * @return Voice* + */ Voice* findFreeVoice() noexcept; + // Names for the cc as set by the cc_label or cc_name opcodes std::vector ccNames; + // Default active switch if multiple keyswitchable regions are present absl::optional defaultSwitch; std::set unknownOpcodes; using RegionPtrVector = std::vector; using VoicePtrVector = std::vector; std::vector> regions; std::vector> voices; + // Views to speed up iteration over the regions and voices when events + // occur in the audio callback VoicePtrVector voiceViewArray; std::array noteActivationLists; std::array ccActivationLists; + // Internal temporary buffer AudioBuffer tempBuffer { 2, config::defaultSamplesPerBlock }; + int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; float volume { Default::volume }; + int numVoices { config::numVoices }; + // Distribution used to generate random value for the *rand opcodes std::uniform_real_distribution randNoteDistribution { 0, 1 }; unsigned fileTicket { 1 }; + // Atomic guards; must be used with AtomicGuard and AtomicDisabler std::atomic canEnterCallback { true }; std::atomic inCallback { false }; - int numVoices { config::numVoices }; - LEAK_DETECTOR(Synth); };