From 2873c970e81fd3a35dcbfbd4048dcf8a02a94ea5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 24 Jun 2020 05:51:20 +0200 Subject: [PATCH 001/445] Audio file incremental reading --- dpf.mk | 1 + src/CMakeLists.txt | 1 + src/sfizz/AudioReader.cpp | 367 ++++++++++++++++++++++++++++++++++++++ src/sfizz/AudioReader.h | 59 ++++++ src/sfizz/FilePool.cpp | 93 ++++------ src/sfizz/Oversampler.cpp | 130 ++++++++++++-- src/sfizz/Oversampler.h | 12 ++ 7 files changed, 594 insertions(+), 69 deletions(-) create mode 100644 src/sfizz/AudioReader.cpp create mode 100644 src/sfizz/AudioReader.h diff --git a/dpf.mk b/dpf.mk index 772dd364..7dd64e45 100644 --- a/dpf.mk +++ b/dpf.mk @@ -56,6 +56,7 @@ sfizz-clean: SFIZZ_SOURCES = \ src/sfizz/ADSREnvelope.cpp \ + src/sfizz/AudioReader.cpp \ src/sfizz/Curve.cpp \ src/sfizz/effects/Apan.cpp \ src/sfizz/Effects.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 67c3b36f..bc2ce9b2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ set (SFIZZ_SOURCES sfizz/FileId.cpp sfizz/FilePool.cpp sfizz/FileInstrument.cpp + sfizz/AudioReader.cpp sfizz/FilterPool.cpp sfizz/EQPool.cpp sfizz/Region.cpp diff --git a/src/sfizz/AudioReader.cpp b/src/sfizz/AudioReader.cpp new file mode 100644 index 00000000..bd397bc1 --- /dev/null +++ b/src/sfizz/AudioReader.cpp @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "AudioReader.h" +#include +#include + +namespace sfz { + +class BasicSndfileReader : public AudioReader { +public: + explicit BasicSndfileReader(SndfileHandle handle) : handle_(handle) {} + virtual ~BasicSndfileReader() {} + + int format() const override; + int64_t frames() const override; + unsigned channels() const override; + unsigned sampleRate() const override; + bool getInstrument(SF_INSTRUMENT* instrument) override; + +protected: + SndfileHandle handle_; +}; + +int BasicSndfileReader::format() const +{ + return handle_.format(); +} + +int64_t BasicSndfileReader::frames() const +{ + return handle_.frames(); +} + +unsigned BasicSndfileReader::channels() const +{ + return handle_.channels(); +} + +unsigned BasicSndfileReader::sampleRate() const +{ + return handle_.samplerate(); +} + +bool BasicSndfileReader::getInstrument(SF_INSTRUMENT* instrument) +{ + if (handle_.command(SFC_GET_INSTRUMENT, instrument, sizeof(SF_INSTRUMENT)) == SF_FALSE) + return false; + return true; +} + +//------------------------------------------------------------------------------ + +/** + * @brief Audio file reader in forward direction + */ +class ForwardReader : public BasicSndfileReader { +public: + explicit ForwardReader(SndfileHandle handle); + AudioReaderType type() const override; + size_t readNextBlock(float* buffer, size_t frames) override; +}; + +ForwardReader::ForwardReader(SndfileHandle handle) + : BasicSndfileReader(handle) +{ +} + +AudioReaderType ForwardReader::type() const +{ + return AudioReaderType::Forward; +} + +size_t ForwardReader::readNextBlock(float* buffer, size_t frames) +{ + sf_count_t readFrames = handle_.readf(buffer, frames); + if (frames <= 0) + return 0; + + return readFrames; +} + +//------------------------------------------------------------------------------ + +template +struct AudioFrame { + T samples[N]; +}; + +/** + * @brief Reorder a sequence of frames in reverse + */ +static void reverse_frames(float* data, sf_count_t frames, unsigned channels) +{ + switch (channels) { + +#define SPECIALIZE_FOR(N) \ + case N: \ + std::reverse( \ + reinterpret_cast *>(data), \ + reinterpret_cast *>(data) + frames); \ + break + + SPECIALIZE_FOR(1); + SPECIALIZE_FOR(2); + + default: + for (sf_count_t i = 0; i < frames / 2; ++i) { + sf_count_t j = frames - 1 - i; + float* frame1 = &data[i * channels]; + float* frame2 = &data[j * channels]; + for (unsigned c = 0; c < channels; ++c) + std::swap(frame1[c], frame2[c]); + } + break; + +#undef SPECIALIZE_FOR + } +} + +//------------------------------------------------------------------------------ + +/** + * @brief Audio file reader in reverse direction, for fast-seeking formats + */ +class ReverseReader : public BasicSndfileReader { +public: + explicit ReverseReader(SndfileHandle handle); + AudioReaderType type() const override; + size_t readNextBlock(float* buffer, size_t frames) override; + +private: + sf_count_t position_ {}; +}; + +ReverseReader::ReverseReader(SndfileHandle handle) + : BasicSndfileReader(handle) +{ + position_ = handle.seek(0, SF_SEEK_END); +} + +AudioReaderType ReverseReader::type() const +{ + return AudioReaderType::Reverse; +} + +size_t ReverseReader::readNextBlock(float* buffer, size_t frames) +{ + sf_count_t position = position_; + const unsigned channels = handle_.channels(); + + const sf_count_t readFrames = std::min(frames, position); + if (readFrames <= 0) + return false; + + position -= readFrames; + if (handle_.seek(position, SEEK_SET) != position || + handle_.readf(buffer, readFrames) != readFrames) + return false; + + position_ = position; + reverse_frames(buffer, readFrames, channels); + return readFrames; +} + +//------------------------------------------------------------------------------ + +/** + * @brief Audio file reader in reverse direction, for slow-seeking formats + */ +class NoSeekReverseReader : public BasicSndfileReader { +public: + explicit NoSeekReverseReader(SndfileHandle handle); + AudioReaderType type() const override; + size_t readNextBlock(float* buffer, size_t frames) override; + +private: + void readWholeFile(); + +private: + std::unique_ptr fileBuffer_; + sf_count_t fileFramesLeft_ { 0 }; +}; + +NoSeekReverseReader::NoSeekReverseReader(SndfileHandle handle) + : BasicSndfileReader(handle) +{ +} + +AudioReaderType NoSeekReverseReader::type() const +{ + return AudioReaderType::NoSeekReverse; +} + +size_t NoSeekReverseReader::readNextBlock(float* buffer, size_t frames) +{ + float* fileBuffer = fileBuffer_.get(); + if (!fileBuffer) { + readWholeFile(); + fileBuffer = fileBuffer_.get(); + } + + const unsigned channels = handle_.channels(); + const sf_count_t fileFramesLeft = fileFramesLeft_; + sf_count_t readFrames = std::min(frames, fileFramesLeft); + if (readFrames <= 0) + return 0; + + std::copy( + &fileBuffer[channels * (fileFramesLeft - readFrames)], + &fileBuffer[channels * fileFramesLeft], buffer); + reverse_frames(buffer, readFrames, channels); + + fileFramesLeft_ = fileFramesLeft - readFrames; + return readFrames; +} + +void NoSeekReverseReader::readWholeFile() +{ + const sf_count_t frames = handle_.frames(); + const unsigned channels = handle_.channels(); + float* fileBuffer = new float[channels * frames]; + fileBuffer_.reset(fileBuffer); + fileFramesLeft_ = handle_.readf(fileBuffer, frames); +} + +//------------------------------------------------------------------------------ + +const std::error_category& sndfile_category() +{ + class sndfile_category : public std::error_category { + public: + const char* name() const noexcept override + { + return "sndfile"; + } + + std::string message(int condition) const override + { + const char* str = sf_error_number(condition); + return str ? str : ""; + } + }; + + static const sndfile_category cat; + return cat; +} + +//------------------------------------------------------------------------------ + +class DummyAudioReader : public AudioReader { +public: + explicit DummyAudioReader(AudioReaderType type) : type_(type) {} + AudioReaderType type() const override { return type_; } + int format() const override { return 0; } + int64_t frames() const override { return 0; } + unsigned channels() const override { return 1; } + unsigned sampleRate() const override { return 44100; } + size_t readNextBlock(float*, size_t) override { return 0; } + bool getInstrument(SF_INSTRUMENT* ) override { return false; } + +private: + AudioReaderType type_ {}; +}; + +//------------------------------------------------------------------------------ + +static bool formatHasFastSeeking(int format) +{ + bool fast; + + const int type = format & SF_FORMAT_TYPEMASK; + const int subtype = format & SF_FORMAT_SUBMASK; + + switch (type) { + case SF_FORMAT_WAV: + case SF_FORMAT_AIFF: + case SF_FORMAT_AU: + case SF_FORMAT_RAW: + case SF_FORMAT_WAVEX: + // TODO: list more PCM formats that support fast seeking + fast = subtype >= SF_FORMAT_PCM_S8 && subtype <= SF_FORMAT_DOUBLE; + break; + case SF_FORMAT_FLAC: + // seeking has acceptable overhead + fast = true; + break; + case SF_FORMAT_OGG: + // ogg is prohibitively slow at seeking (possibly others) + // cf. https://github.com/erikd/libsndfile/issues/491 + fast = false; + break; + default: + fast = false; + break; + } + + return fast; +} + +AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec) +{ + AudioReaderPtr reader; + + if (ec) + ec->clear(); + +#if defined(_WIN32) + SndfileHandle handle(path.wstring().c_str()); +#else + SndfileHandle handle(path.c_str()); +#endif + + if (!handle) { + if (ec) + *ec = std::error_code(handle.error(), sndfile_category()); + reader.reset(new DummyAudioReader(reverse ? AudioReaderType::Reverse : AudioReaderType::Forward)); + } + else if (!reverse) + reader.reset(new ForwardReader(handle)); + else if (formatHasFastSeeking(handle.format())) + reader.reset(new ReverseReader(handle)); + else + reader.reset(new NoSeekReverseReader(handle)); + + return reader; +} + +AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec) +{ + AudioReaderPtr reader; + + if (ec) + ec->clear(); + +#if defined(_WIN32) + SndfileHandle handle(path.wstring().c_str()); +#else + SndfileHandle handle(path.c_str()); +#endif + + if (!handle) { + if (ec) + *ec = std::error_code(handle.error(), sndfile_category()); + reader.reset(new DummyAudioReader(type)); + } + else { + switch (type) { + case AudioReaderType::Forward: + reader.reset(new ForwardReader(handle)); + break; + case AudioReaderType::Reverse: + reader.reset(new ReverseReader(handle)); + break; + case AudioReaderType::NoSeekReverse: + reader.reset(new NoSeekReverseReader(handle)); + break; + } + } + + return reader; +} + +} // namespace sfz diff --git a/src/sfizz/AudioReader.h b/src/sfizz/AudioReader.h new file mode 100644 index 00000000..653abce3 --- /dev/null +++ b/src/sfizz/AudioReader.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "absl/types/span.h" +#include "ghc/fs_std.hpp" +#include +#include +#if defined(_WIN32) +#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 +#include +#endif +#include + +namespace sfz { + +/** + * @brief Designation of a particular kind of audio reader + */ +enum class AudioReaderType { + //! Reader in forward direction + Forward, + //! Reader in reverse direction + Reverse, + //! Reader in reverse direction, operating on a whole file instead of seeking + NoSeekReverse, +}; + +/** + * @brief Reader of audio file data + */ +class AudioReader { +public: + virtual ~AudioReader() {} + virtual AudioReaderType type() const = 0; + virtual int format() const = 0; + virtual int64_t frames() const = 0; + virtual unsigned channels() const = 0; + virtual unsigned sampleRate() const = 0; + virtual size_t readNextBlock(float* buffer, size_t frames) = 0; + virtual bool getInstrument(SF_INSTRUMENT* instrument) = 0; +}; + +typedef std::unique_ptr AudioReaderPtr; + +/** + * @brief Create a file reader of detected type. + */ +AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec = nullptr); + +/** + * @brief Create a file reader of explicit type. (for testing purposes) + */ +AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec = nullptr); + +} // namespace sfz diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 95519ac6..af9f50c7 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -24,6 +24,7 @@ // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "FilePool.h" +#include "AudioReader.h" #include "FileInstrument.h" #include "Buffer.h" #include "AudioBuffer.h" @@ -40,69 +41,50 @@ #include #include -void readBaseFile(SndfileHandle& sndFile, sfz::FileAudioBuffer& output, uint32_t numFrames, bool reverse) +void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32_t numFrames) { output.reset(); output.resize(numFrames); - if (reverse) - sndFile.seek(-static_cast(numFrames), SEEK_END); - - const unsigned channels = sndFile.channels(); + const unsigned channels = reader.channels(); if (channels == 1) { output.addChannel(); output.clear(); - sndFile.readf(output.channelWriter(0), numFrames); + reader.readNextBlock(output.channelWriter(0), numFrames); } else if (channels == 2) { output.addChannel(); output.addChannel(); output.clear(); sfz::Buffer tempReadBuffer { 2 * numFrames }; - sndFile.readf(tempReadBuffer.data(), numFrames); + reader.readNextBlock(tempReadBuffer.data(), numFrames); sfz::readInterleaved(tempReadBuffer, output.getSpan(0), output.getSpan(1)); } - - if (reverse) { - for (unsigned c = 0; c < channels; ++c) { - // TODO: consider optimizing with SIMD - absl::Span channel = output.getSpan(c); - std::reverse(channel.begin(), channel.end()); - } - } } -std::unique_ptr readFromFile(SndfileHandle& sndFile, uint32_t numFrames, sfz::Oversampling factor, bool reverse) +std::unique_ptr readFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor) { auto baseBuffer = absl::make_unique(); - readBaseFile(sndFile, *baseBuffer, numFrames, reverse); + readBaseFile(reader, *baseBuffer, numFrames); if (factor == sfz::Oversampling::x1) return baseBuffer; - auto outputBuffer = absl::make_unique(sndFile.channels(), numFrames * static_cast(factor)); + auto outputBuffer = absl::make_unique(reader.channels(), numFrames * static_cast(factor)); outputBuffer->clear(); sfz::Oversampler oversampler { factor }; oversampler.stream(*baseBuffer, *outputBuffer); return outputBuffer; } -void streamFromFile(SndfileHandle& sndFile, uint32_t numFrames, sfz::Oversampling factor, bool reverse, sfz::FileAudioBuffer& output, std::atomic* filledFrames = nullptr) +void streamFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor, sfz::FileAudioBuffer& output, std::atomic* filledFrames = nullptr) { - if (factor == sfz::Oversampling::x1) { - readBaseFile(sndFile, output, numFrames, reverse); - if (filledFrames != nullptr) - filledFrames->store(numFrames); - return; - } - - auto baseBuffer = readFromFile(sndFile, numFrames, sfz::Oversampling::x1, reverse); output.reset(); - output.addChannels(baseBuffer->getNumChannels()); + output.addChannels(reader.channels()); output.resize(numFrames * static_cast(factor)); output.clear(); sfz::Oversampler oversampler { factor }; - oversampler.stream(*baseBuffer, output, filledFrames); + oversampler.stream(reader, output, filledFrames); } sfz::FilePool::FilePool(sfz::Logger& logger) @@ -210,24 +192,26 @@ absl::optional sfz::FilePool::getFileInformation(const Fil if (!fs::exists(file)) return {}; - SndfileHandle sndFile(file.string().c_str()); - if (sndFile.channels() != 1 && sndFile.channels() != 2) { + AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); + const unsigned channels = reader->channels(); + + if (channels != 1 && channels != 2) { DBG("[sfizz] Missing logic for " << sndFile.channels() << " channels, discarding sample " << fileId); return {}; } FileInformation returnedValue; - returnedValue.end = static_cast(sndFile.frames()) - 1; - returnedValue.sampleRate = static_cast(sndFile.samplerate()); - returnedValue.numChannels = sndFile.channels(); + returnedValue.end = static_cast(reader->frames()) - 1; + returnedValue.sampleRate = static_cast(reader->sampleRate()); + returnedValue.numChannels = reader->channels(); SF_INSTRUMENT instrumentInfo {}; - const int sndFormat = sndFile.format(); + const int sndFormat = reader->format(); if ((sndFormat & SF_FORMAT_TYPEMASK) == SF_FORMAT_FLAC) sfz::FileInstruments::extractFromFlac(file, instrumentInfo); else - sndFile.command(SFC_GET_INSTRUMENT, &instrumentInfo, sizeof(instrumentInfo)); + reader->getInstrument(&instrumentInfo); if (!fileId.isReverse()) { if (instrumentInfo.loop_count > 0) { @@ -249,10 +233,10 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce return false; const fs::path file { rootDirectory / fileId.filename() }; - SndfileHandle sndFile(file.string().c_str()); + AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); // FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same - const auto frames = static_cast(sndFile.frames()); + const auto frames = static_cast(reader->frames()); const auto framesToLoad = [&]() { if (preloadSize == 0) return frames; @@ -263,12 +247,12 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce const auto existingFile = preloadedFiles.find(fileId); if (existingFile != preloadedFiles.end()) { if (framesToLoad > existingFile->second.preloadedData->getNumFrames()) { - preloadedFiles[fileId].preloadedData = readFromFile(sndFile, framesToLoad, oversamplingFactor, fileId.isReverse()); + preloadedFiles[fileId].preloadedData = readFromFile(*reader, framesToLoad, oversamplingFactor); } } else { - fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(sndFile.samplerate()); + fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(reader->sampleRate()); FileDataHandle handle { - readFromFile(sndFile, framesToLoad, oversamplingFactor, fileId.isReverse()), + readFromFile(*reader, framesToLoad, oversamplingFactor), *fileInformation }; preloadedFiles.insert_or_assign(fileId, handle); @@ -283,17 +267,17 @@ absl::optional sfz::FilePool::loadFile(const FileId& fileId return {}; const fs::path file { rootDirectory / fileId.filename() }; - SndfileHandle sndFile(file.string().c_str()); + AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); // FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same - const auto frames = static_cast(sndFile.frames()); + const auto frames = static_cast(reader->frames()); const auto existingFile = loadedFiles.find(fileId); if (existingFile != loadedFiles.end()) { return existingFile->second; } else { - fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(sndFile.samplerate()); + fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(reader->sampleRate()); FileDataHandle handle { - readFromFile(sndFile, frames, oversamplingFactor, fileId.isReverse()), + readFromFile(*reader, frames, oversamplingFactor), *fileInformation }; loadedFiles.insert_or_assign(fileId, handle); @@ -342,8 +326,8 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept const auto numFrames = preloadedFile.second.preloadedData->getNumFrames() / static_cast(oversamplingFactor); const auto maxOffset = numFrames > this->preloadSize ? static_cast(numFrames) - this->preloadSize : 0; fs::path file { rootDirectory / preloadedFile.first.filename() }; - SndfileHandle sndFile(file.string().c_str()); - preloadedFile.second.preloadedData = readFromFile(sndFile, preloadSize + maxOffset, oversamplingFactor, preloadedFile.first.isReverse()); + AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); + preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, oversamplingFactor); } this->preloadSize = preloadSize; } @@ -392,14 +376,15 @@ void sfz::FilePool::loadingThread() noexcept const auto waitDuration = loadStartTime - promise->creationTime; const fs::path file { rootDirectory / promise->fileId.filename() }; - SndfileHandle sndFile(file.string().c_str()); - if (sndFile.error() != 0) { - DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << sndFile.strError()); + std::error_code readError; + AudioReaderPtr reader = createAudioReader(file, promise->fileId.isReverse(), &readError); + if (readError) { + DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << readError.what()); promise->dataStatus = FilePromise::DataStatus::Error; continue; } - const auto frames = static_cast(sndFile.frames()); - streamFromFile(sndFile, frames, oversamplingFactor, promise->fileId.isReverse(), promise->fileData, &promise->availableFrames); + const auto frames = static_cast(reader->frames()); + streamFromFile(*reader, frames, oversamplingFactor, promise->fileData, &promise->availableFrames); promise->dataStatus = FilePromise::DataStatus::Ready; const auto loadDuration = std::chrono::high_resolution_clock::now() - loadStartTime; logger.logFileTime(waitDuration, loadDuration, frames, promise->fileId.filename()); @@ -453,8 +438,8 @@ void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept const auto numFrames = preloadedFile.second.preloadedData->getNumFrames() / static_cast(this->oversamplingFactor); const uint32_t maxOffset = numFrames > this->preloadSize ? static_cast(numFrames) - this->preloadSize : 0; fs::path file { rootDirectory / preloadedFile.first.filename() }; - SndfileHandle sndFile(file.string().c_str()); - preloadedFile.second.preloadedData = readFromFile(sndFile, preloadSize + maxOffset, factor, preloadedFile.first.isReverse()); + AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); + preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, factor); preloadedFile.second.information.sampleRate *= samplerateChange; } diff --git a/src/sfizz/Oversampler.cpp b/src/sfizz/Oversampler.cpp index 1027b8ba..67de1c30 100644 --- a/src/sfizz/Oversampler.cpp +++ b/src/sfizz/Oversampler.cpp @@ -7,6 +7,7 @@ #include "Oversampler.h" #include "Buffer.h" #include "AudioSpan.h" +#include "AudioReader.h" #include "SIMDConfig.h" constexpr std::array coeffsStage2x { @@ -69,28 +70,29 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s const auto numFrames = input.getNumFrames(); const auto numChannels = input.getNumChannels(); - std::vector upsampler2x (numChannels); - std::vector upsampler4x (numChannels); - std::vector upsampler8x (numChannels); + std::vector upsampler2x; + std::vector upsampler4x; + std::vector upsampler8x; switch(factor) { case Oversampling::x8: + upsampler8x.resize(numChannels); for (auto& upsampler: upsampler8x) upsampler.set_coefs(coeffsStage8x.data()); // fallthrough case Oversampling::x4: + upsampler4x.resize(numChannels); for (auto& upsampler: upsampler4x) upsampler.set_coefs(coeffsStage4x.data()); // fallthrough case Oversampling::x2: + upsampler2x.resize(numChannels); for (auto& upsampler: upsampler2x) upsampler.set_coefs(coeffsStage2x.data()); break; case Oversampling::x1: - for (size_t i = 0; i < numChannels; ++i) - copy(input.getConstSpan(i), output.getSpan(i).first(numFrames)); - return; + break; } // Intermediate buffers @@ -109,20 +111,119 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s for (size_t chanIdx = 0; chanIdx < numChannels; chanIdx++) { const auto inputChunk = input.getSpan(chanIdx).subspan(inputFrameCounter, thisChunkSize); const auto outputChunk = output.getSpan(chanIdx).subspan(outputFrameCounter, outputChunkSize); - if (factor == Oversampling::x2) { + switch (factor) { + case Oversampling::x1: + copy(inputChunk, outputChunk); + break; + case Oversampling::x2: upsampler2x[chanIdx].process_block(outputChunk.data(), inputChunk.data(), static_cast(thisChunkSize)); - continue; - } - if (factor == Oversampling::x4) { + break; + case Oversampling::x4: upsampler2x[chanIdx].process_block(span1.data(), inputChunk.data(), static_cast(thisChunkSize)); upsampler4x[chanIdx].process_block(outputChunk.data(), span1.data(), static_cast(thisChunkSize * 2)); - continue; - } - else if (factor == Oversampling::x8) { + break; + case Oversampling::x8: upsampler2x[chanIdx].process_block(span1.data(), inputChunk.data(), static_cast(thisChunkSize)); upsampler4x[chanIdx].process_block(span2.data(), span1.data(), static_cast(thisChunkSize * 2)); upsampler8x[chanIdx].process_block(outputChunk.data(), span2.data(), static_cast(thisChunkSize * 4)); - continue; + break; + } + } + inputFrameCounter += thisChunkSize; + outputFrameCounter += outputChunkSize; + + if (framesReady != nullptr) + framesReady->fetch_add(outputChunkSize); + } +} + +void sfz::Oversampler::stream(AudioReader& input, AudioSpan output, std::atomic* framesReady) +{ + ASSERT(output.getNumFrames() >= input.getNumFrames() * static_cast(factor)); + ASSERT(output.getNumChannels() == input.getNumChannels()); + + const auto numFrames = static_cast(input.frames()); + const auto numChannels = input.channels(); + + std::vector upsampler2x; + std::vector upsampler4x; + std::vector upsampler8x; + + switch(factor) + { + case Oversampling::x8: + upsampler8x.resize(numChannels); + for (auto& upsampler: upsampler8x) + upsampler.set_coefs(coeffsStage8x.data()); + // fallthrough + case Oversampling::x4: + upsampler4x.resize(numChannels); + for (auto& upsampler: upsampler4x) + upsampler.set_coefs(coeffsStage4x.data()); + // fallthrough + case Oversampling::x2: + upsampler2x.resize(numChannels); + for (auto& upsampler: upsampler2x) + upsampler.set_coefs(coeffsStage2x.data()); + break; + case Oversampling::x1: + break; + } + + // Intermediate buffers + sfz::Buffer fileBlock { chunkSize * numChannels }; + sfz::Buffer buffer1 { chunkSize * 2 }; + sfz::Buffer buffer2 { chunkSize * 4 }; + auto span1 = absl::MakeSpan(buffer1); + auto span2 = absl::MakeSpan(buffer2); + + auto upsample2xFromInterleaved = [numChannels]( + Upsampler2x& upsampler, float* output, const float* input, + size_t numInputFrames, unsigned chanIdx) + { + for (size_t i = 0; i < numInputFrames; ++i) { + float* outp = &output[2 * i]; + const float* inp = &input[i * numChannels + chanIdx]; + upsampler.process_sample(outp[0], outp[1], inp[0]); + } + }; + + size_t inputFrameCounter { 0 }; + size_t outputFrameCounter { 0 }; + bool inputEof = false; + while (!inputEof && inputFrameCounter < numFrames) + { + // std::cout << "Input frames: " << inputFrameCounter << "/" << numFrames << '\n'; + auto thisChunkSize = std::min(chunkSize, numFrames - inputFrameCounter); + const auto numFramesRead = static_cast( + input.readNextBlock(fileBlock.data(), thisChunkSize)); + if (numFramesRead == 0) + break; + if (numFramesRead < thisChunkSize) { + inputEof = true; + thisChunkSize = numFramesRead; + } + const auto outputChunkSize = thisChunkSize * static_cast(factor); + + for (size_t chanIdx = 0; chanIdx < numChannels; chanIdx++) { + const auto outputChunk = output.getSpan(chanIdx).subspan(outputFrameCounter, outputChunkSize); + switch (factor) { + case Oversampling::x1: + for (size_t i = 0; i < thisChunkSize; ++i) + outputChunk[i] = fileBlock[i * numChannels + chanIdx]; + break; + case Oversampling::x2: + upsample2xFromInterleaved(upsampler2x[chanIdx], outputChunk.data(), fileBlock.data(), thisChunkSize, chanIdx); + break; + case Oversampling::x4: + upsample2xFromInterleaved(upsampler2x[chanIdx], span1.data(), fileBlock.data(), thisChunkSize, chanIdx); + upsampler4x[chanIdx].process_block(outputChunk.data(), span1.data(), static_cast(thisChunkSize * 2)); + break; + case Oversampling::x8: + upsample2xFromInterleaved(upsampler2x[chanIdx], span1.data(), fileBlock.data(), thisChunkSize, chanIdx); + upsampler4x[chanIdx].process_block(span2.data(), span1.data(), static_cast(thisChunkSize * 2)); + upsampler8x[chanIdx].process_block(outputChunk.data(), span2.data(), static_cast(thisChunkSize * 4)); + break; } } inputFrameCounter += thisChunkSize; @@ -131,5 +232,4 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s if (framesReady != nullptr) framesReady->fetch_add(outputChunkSize); } - } diff --git a/src/sfizz/Oversampler.h b/src/sfizz/Oversampler.h index 25e74eaf..f6890ad2 100644 --- a/src/sfizz/Oversampler.h +++ b/src/sfizz/Oversampler.h @@ -15,6 +15,8 @@ #include "Config.h" namespace sfz { +class AudioReader; + /** * @brief Wraps the internal oversampler in a single function that takes an * AudioBuffer and oversamples it in another pre-allocated one. The @@ -41,6 +43,16 @@ public: * @param framesReady an atomic counter for the ready frames. If null no signaling is done. */ void stream(AudioSpan input, AudioSpan output, std::atomic* framesReady = nullptr); + /** + * @brief Stream the oversampling of an input AudioReader into an output + * one, possibly signaling the caller along the way of the number of + * frames that are written. + * + * @param input + * @param output + * @param framesReady an atomic counter for the ready frames. If null no signaling is done. + */ + void stream(AudioReader& input, AudioSpan output, std::atomic* framesReady = nullptr); Oversampler() = delete; Oversampler(const Oversampler&) = delete; From f889e9e754f6f1b243e1fd94ae9efe1be4424d7e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 15 Jul 2020 11:55:33 +0200 Subject: [PATCH 002/445] Add the spinlock mutex --- dpf.mk | 1 + src/CMakeLists.txt | 2 ++ src/sfizz/utility/SpinMutex.cpp | 43 +++++++++++++++++++++++++++ src/sfizz/utility/SpinMutex.h | 28 ++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/ConcurrencyT.cpp | 51 +++++++++++++++++++++++++++++++++ 6 files changed, 126 insertions(+) create mode 100644 src/sfizz/utility/SpinMutex.cpp create mode 100644 src/sfizz/utility/SpinMutex.h create mode 100644 tests/ConcurrencyT.cpp diff --git a/dpf.mk b/dpf.mk index 772dd364..ba5d18c3 100644 --- a/dpf.mk +++ b/dpf.mk @@ -101,6 +101,7 @@ SFIZZ_SOURCES = \ src/sfizz/simd/HelpersAVX.cpp \ src/sfizz/Synth.cpp \ src/sfizz/Tuning.cpp \ + src/sfizz/utility/SpinMutex.cpp \ src/sfizz/Voice.cpp \ src/sfizz/Wavetables.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8de6d8eb..23d35477 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,8 @@ set (SFIZZ_HEADERS sfizz/Config.h sfizz/Curve.h sfizz/Debug.h + sfizz/utility/SpinMutex.h + sfizz/utility/SpinMutex.cpp sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArraySSE.h diff --git a/src/sfizz/utility/SpinMutex.cpp b/src/sfizz/utility/SpinMutex.cpp new file mode 100644 index 00000000..cb416081 --- /dev/null +++ b/src/sfizz/utility/SpinMutex.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SpinMutex.h" +#include +#include + +// based on Timur Doumler's implementation advice for spinlocks + +void SpinMutex::lock() noexcept +{ + for (int i = 0; i < 5; ++i) { + if (try_lock()) + return; + } + + for (int i = 0; i < 10; ++i) { + if (try_lock()) + return; + atomic_queue::spin_loop_pause(); + } + + for (;;) { + for (int i = 0; i < 3000; ++i) { + if (try_lock()) + return; + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + } + std::this_thread::yield(); + } +} diff --git a/src/sfizz/utility/SpinMutex.h b/src/sfizz/utility/SpinMutex.h new file mode 100644 index 00000000..5e1aa347 --- /dev/null +++ b/src/sfizz/utility/SpinMutex.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include + +class SpinMutex { +public: + void lock() noexcept; + bool try_lock() noexcept; + void unlock() noexcept; + +private: + std::atomic_flag flag_ = ATOMIC_FLAG_INIT; +}; + +inline bool SpinMutex::try_lock() noexcept +{ + return !flag_.test_and_set(std::memory_order_acquire); +} + +inline void SpinMutex::unlock() noexcept +{ + flag_.clear(std::memory_order_release); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 31e134c8..21bc72d1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,7 @@ set(SFIZZ_TEST_SOURCES SemaphoreT.cpp SwapAndPopT.cpp TuningT.cpp + ConcurrencyT.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) diff --git a/tests/ConcurrencyT.cpp b/tests/ConcurrencyT.cpp new file mode 100644 index 00000000..9ec72cd5 --- /dev/null +++ b/tests/ConcurrencyT.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/utility/SpinMutex.h" +#include "catch2/catch.hpp" +#include +#include +#include + +TEST_CASE("[SpinMutex] Basic synchronization") +{ + constexpr size_t num_threads = 8; + constexpr size_t num_iterations = 1000000; + + volatile size_t counter = 0; + SpinMutex counter_mutex; + + std::thread threads[num_threads]; + + volatile bool ready = false; + std::condition_variable ready_cv; + std::mutex ready_mtx; + + auto thread_run = [&]() + { + std::unique_lock lock(ready_mtx); + ready_cv.wait(lock, [&]() -> bool { return ready; }); + lock.unlock(); + + for (size_t i = 0; i < num_iterations; ++i) { + std::unique_lock lock(counter_mutex); + ++counter; + } + }; + + for (unsigned i = 0; i < num_threads; ++i) + threads[i] = std::thread(thread_run); + + std::unique_lock lock(ready_mtx); + ready = true; + ready_cv.notify_all(); + lock.unlock(); + + for (unsigned i = 0; i < num_threads; ++i) + threads[i].join(); + + REQUIRE(counter == num_threads * num_iterations); +} From 2d7e81edefb9f28e39a70c4d059bbe6803c332f7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 19 Jul 2020 23:09:36 +0200 Subject: [PATCH 003/445] Support for `sustain_lo` --- src/sfizz/Config.h | 1 - src/sfizz/Defaults.h | 2 ++ src/sfizz/Region.cpp | 9 +++++++-- src/sfizz/Region.h | 3 ++- src/sfizz/Voice.cpp | 4 ++-- tests/RegionT.cpp | 13 +++++++++++++ tests/SynthT.cpp | 32 ++++++++++++++++++++++++++++++++ 7 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index db286b01..60c2f0a2 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -50,7 +50,6 @@ namespace config { constexpr int allNotesOffCC { 123 }; constexpr int omniOffCC { 124 }; constexpr int omniOnCC { 125 }; - constexpr float halfCCThreshold { 0.5f }; constexpr int centPerSemitone { 100 }; constexpr float virtuallyZero { 0.001f }; constexpr float fastReleaseDuration { 0.01f }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 940b6872..8be2fc96 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -57,6 +57,7 @@ namespace Default // common defaults constexpr Range midi7Range { 0, 127 }; + constexpr Range float7Range { 0.0f, 127.0f }; constexpr Range normalizedRange { 0.0f, 1.0f }; constexpr Range symmetricNormalizedRange { -1.0, 1.0 }; @@ -216,6 +217,7 @@ namespace Default constexpr float start { 0.0 }; constexpr float sustain { 100.0 }; constexpr uint16_t sustainCC { 64 }; + constexpr float sustainThreshold { 0.0039f }; // sforzando default (0.5f/127.0f) constexpr float vel2sustain { 0.0 }; constexpr int depth { 0 }; constexpr Range egTimeRange { 0.0, 100.0 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 1f24931c..0a19625c 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -281,6 +281,11 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("sustain_cc"): setValueFromOpcode(opcode, sustainCC, Default::ccNumberRange); break; + case hash("sustain_lo"): + if (auto value = readOpcode(opcode.value, Default::float7Range)) { + sustainThreshold = normalizeCC(*value); + } + break; case hash("sustain_sw"): checkSustain = readBooleanFromOpcode(opcode).value_or(Default::checkSustain); break; @@ -1038,7 +1043,7 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu const bool randOk = randRange.contains(randValue); bool releaseTrigger = (trigger == SfzTrigger::release_key); if (trigger == SfzTrigger::release) { - if (midiState.getCCValue(sustainCC) < config::halfCCThreshold) + if (midiState.getCCValue(sustainCC) < sustainThreshold) releaseTrigger = true; else noteIsOff = true; @@ -1057,7 +1062,7 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept if (!isSwitchedOn()) return false; - if (sustainCC == ccNumber && ccValue < config::halfCCThreshold && noteIsOff) { + if (sustainCC == ccNumber && ccValue < sustainThreshold && noteIsOff) { noteIsOff = false; return true; } diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 322f96ec..772ee399 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -306,6 +306,7 @@ struct Region { bool checkSustain { Default::checkSustain }; // sustain_sw bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw uint16_t sustainCC { Default::sustainCC }; // sustain_cc + float sustainThreshold { Default::sustainThreshold }; // sustain_cc // Region logic: internal conditions Range aftertouchRange { Default::aftertouchRange }; // hichanaft and lochanaft @@ -372,7 +373,7 @@ struct Region { bool triggerOnCC { false }; // whether the region triggers on CC events or note events bool triggerOnNote { true }; - + // Parent RegionSet* parent { nullptr }; private: diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index adfaa718..6dff257b 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -206,7 +206,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex if (region->loopMode == SfzLoopMode::one_shot) return; - if (!region->checkSustain || resources.midiState.getCCValue(region->sustainCC) < config::halfCCThreshold) + if (!region->checkSustain || resources.midiState.getCCValue(region->sustainCC) < region->sustainThreshold) release(delay); } } @@ -220,7 +220,7 @@ void sfz::Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept if (state != State::playing) return; - if (region->checkSustain && noteIsOff && ccNumber == region->sustainCC && ccValue < config::halfCCThreshold) + if (region->checkSustain && noteIsOff && ccNumber == region->sustainCC && ccValue < region->sustainThreshold) release(delay); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index bce1012a..ecfea499 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1216,6 +1216,19 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.sustainCC == 0); } + SECTION("sustain_lo") + { + REQUIRE(region.sustainThreshold == Approx(0.5_norm).margin(1e-3)); + region.parseOpcode({ "sustain_lo", "-1" }); + REQUIRE(region.sustainThreshold == 0_norm); + region.parseOpcode({ "sustain_lo", "1" }); + REQUIRE(region.sustainThreshold == 1_norm); + region.parseOpcode({ "sustain_lo", "63" }); + REQUIRE(region.sustainThreshold == 63_norm); + region.parseOpcode({ "sustain_lo", "128" }); + REQUIRE(region.sustainThreshold == 127_norm); + } + SECTION("Filter stacking and cutoffs") { REQUIRE(region.filters.empty()); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index c0d761c9..bd6b847b 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -663,3 +663,35 @@ TEST_CASE("[Synth] Release (Different sustain CC)") synth.cc(0, 54, 0); REQUIRE( synth.getNumActiveVoices() == 1 ); } + +TEST_CASE("[Synth] Sustain threshold default") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 1); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 0 ); +} + +TEST_CASE("[Synth] Sustain threshold") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sustain_lo=63 + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 1); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 64); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); +} From fe4e5ffd6a16e3f0021dcec76a19db3fbbaf6507 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 24 Jul 2020 00:06:59 +0200 Subject: [PATCH 004/445] Set develop to 0.4.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 205671ac..cbc3945b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ else() endif() endif() -project (sfizz VERSION 0.4.0 LANGUAGES CXX C) +project (sfizz VERSION 0.4.1 LANGUAGES CXX C) set (PROJECT_DESCRIPTION "A library to load SFZ description files and use them to render music.") # External configuration CMake scripts From f6b549d0dbc7e686bed3750f1399a40fa4161a9e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 24 Jul 2020 23:22:56 +0200 Subject: [PATCH 005/445] Allow opcodes to print in test outputs --- src/sfizz/Opcode.cpp | 6 ++++++ src/sfizz/Opcode.h | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/sfizz/Opcode.cpp b/src/sfizz/Opcode.cpp index f7261941..52b3ec66 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -9,6 +9,7 @@ #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" +#include #include #include @@ -154,3 +155,8 @@ absl::optional sfz::readNoteValue(absl::string_view value) return static_cast(noteNumber); } + +std::ostream &operator<<(std::ostream &os, const sfz::Opcode &opcode) +{ + return os << opcode.opcode << '=' << '"' << opcode.value << '"'; +} diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index cd31c8c6..87b25666 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -16,6 +16,7 @@ #include #include #include +#include // charconv support is still sketchy with clang/gcc so we use abseil's numbers #include "absl/strings/numbers.h" @@ -292,3 +293,5 @@ inline void setCCPairFromOpcode(const Opcode& opcode, absl::optional Date: Sun, 26 Jul 2020 00:48:43 +0200 Subject: [PATCH 006/445] Add a benchmark --- benchmarks/BM_audioReaders.cpp | 229 +++++++++++++++++++++++++++++++++ benchmarks/CMakeLists.txt | 3 + src/sfizz/AudioReader.cpp | 46 +++++-- src/sfizz/AudioReader.h | 11 ++ 4 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 benchmarks/BM_audioReaders.cpp diff --git a/benchmarks/BM_audioReaders.cpp b/benchmarks/BM_audioReaders.cpp new file mode 100644 index 00000000..874ff8d2 --- /dev/null +++ b/benchmarks/BM_audioReaders.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "AudioReader.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#include +#else +#include +#endif + +/// +struct AutoFD { + AutoFD() {} + ~AutoFD() { reset(); } + + AutoFD(const AutoFD&) = delete; + AutoFD &operator=(const AutoFD&) = delete; + + AutoFD(AutoFD&& other) : fd_(other.fd_) { other.fd_ = -1; } + AutoFD &operator=(AutoFD&& other) + { + if (this == &other) return *this; + reset(other.fd_); + other.fd_ = -1; + return *this; + } + + explicit operator bool() const noexcept { return fd_ != -1; } + int get() const noexcept { return fd_; } + + int release() + { + int fd = fd_; + fd_ = -1; + return fd; + } + + void reset(int fd = -1) noexcept + { + if (fd_ == fd) return; + if (fd_ != -1) close(fd_); + fd_ = fd; + } + +private: + int fd_ = -1; +}; + +/// +class AudioReaderFixture : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) override + { + workBuffer.resize(2 * static_cast(state.range(0))); + } + + void TearDown(const ::benchmark::State& /* state */) override + { + } + + static AutoFD createAudioFile(int format); + + static AutoFD fileWav; + static AutoFD fileFlac; + static AutoFD fileOgg; + + std::vector workBuffer; +}; + +AutoFD AudioReaderFixture::fileWav = createAudioFile(SF_FORMAT_WAV|SF_FORMAT_PCM_16); +AutoFD AudioReaderFixture::fileFlac = createAudioFile(SF_FORMAT_FLAC|SF_FORMAT_PCM_16); +AutoFD AudioReaderFixture::fileOgg = createAudioFile(SF_FORMAT_OGG|SF_FORMAT_VORBIS); + +AutoFD AudioReaderFixture::createAudioFile(int format) +{ + constexpr unsigned sampleRate = 44100; + constexpr unsigned fileDuration = 10; + constexpr unsigned fileFrames = sampleRate * fileDuration; + + // synth 2 channels of arbitrary waveform + std::unique_ptr sndData { new double[2 * fileFrames] }; + double phase = 0.0; + for (unsigned i = 0; i < fileFrames; ++i) { + sndData[2 * i ] = std::sin(2.0 * M_PI * phase); + sndData[2 * i + 1] = std::cos(2.0 * M_PI * phase); + phase += 440.0 * (1.0 / sampleRate); + phase -= static_cast(phase); + } + + // create anonymous temp file + FILE* file = tmpfile(); + if (!file) + throw std::system_error(errno, std::generic_category()); + + // convert FILE to fd, for sndfile + AutoFD fd; + fd.reset(dup(fileno(file))); + if (!fd) { + fclose(file); + throw std::system_error(errno, std::generic_category()); + } + fclose(file); + + // write to fd + SndfileHandle snd(fd.get(), false, SFM_WRITE, format, 2, sampleRate); + if (snd.error()) + throw std::runtime_error("cannot open sound file for writing"); + snd.writef(sndData.get(), fileFrames); + snd = SndfileHandle(); + + return fd; +} + +static void rewindFd(int fd) +{ +#ifndef _WIN32 + off_t off = lseek(fd, 0, SEEK_SET); +#else + off_t off = _lseek(fd, 0, SEEK_SET); +#endif + if (off == -1) + throw std::system_error(errno, std::generic_category()); +} + +static void doReaderBenchmark(int fd, std::vector &buffer, sfz::AudioReaderType type) +{ + rewindFd(fd); + sfz::AudioReaderPtr reader = sfz::createExplicitAudioReaderWithFd(fd, type); + while (reader->readNextBlock(buffer.data(), buffer.size() / 2) > 0); +} + +static void doEntireRead(int fd) +{ + rewindFd(fd); + + SndfileHandle handle(fd, false); + if (handle.error()) + throw std::runtime_error("cannot open sound file for reading"); + + std::vector buffer(static_cast(2 * handle.frames())); + handle.read(buffer.data(), buffer.size()); +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireWav)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileWav.get()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardWav)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileWav.get(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseWav)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileWav.get(), workBuffer, sfz::AudioReaderType::Reverse); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileFlac.get()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileFlac.get(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileFlac.get(), workBuffer, sfz::AudioReaderType::Reverse); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireOgg)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileOgg.get()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardOgg)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileOgg.get(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +//BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseOgg)(benchmark::State& state) +//{ +// for (auto _ : state) { +// doReaderBenchmark(fileOgg.get(), workBuffer, sfz::AudioReaderType::Reverse); +// } +//} + +BENCHMARK_REGISTER_F(AudioReaderFixture, ForwardWav)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, ReverseWav)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, EntireWav)->Range(1, 1); +BENCHMARK_REGISTER_F(AudioReaderFixture, ForwardFlac)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, ReverseFlac)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, EntireFlac)->Range(1, 1); +BENCHMARK_REGISTER_F(AudioReaderFixture, ForwardOgg)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +//BENCHMARK_REGISTER_F(AudioReaderFixture, ReverseOgg)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, EntireOgg)->Range(1, 1); +BENCHMARK_MAIN(); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 3c0082a1..cb38feb6 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -78,6 +78,9 @@ target_link_libraries(bm_wavfile PRIVATE sfizz-sndfile) sfizz_add_benchmark(bm_flacfile BM_flacfile.cpp) target_link_libraries(bm_flacfile PRIVATE sfizz-sndfile) +sfizz_add_benchmark(bm_audioReaders BM_audioReaders.cpp ../src/sfizz/AudioReader.cpp) +target_link_libraries(bm_audioReaders PRIVATE sfizz-sndfile) + sfizz_add_benchmark(bm_readChunk BM_readChunk.cpp) target_link_libraries(bm_readChunk PRIVATE sfizz-sndfile) sfizz_add_benchmark(bm_readChunkFlac BM_readChunkFlac.cpp) diff --git a/src/sfizz/AudioReader.cpp b/src/sfizz/AudioReader.cpp index bd397bc1..9122ee76 100644 --- a/src/sfizz/AudioReader.cpp +++ b/src/sfizz/AudioReader.cpp @@ -301,19 +301,13 @@ static bool formatHasFastSeeking(int format) return fast; } -AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec) +static AudioReaderPtr createAudioReaderWithHandle(SndfileHandle handle, bool reverse, std::error_code* ec) { AudioReaderPtr reader; if (ec) ec->clear(); -#if defined(_WIN32) - SndfileHandle handle(path.wstring().c_str()); -#else - SndfileHandle handle(path.c_str()); -#endif - if (!handle) { if (ec) *ec = std::error_code(handle.error(), sndfile_category()); @@ -329,18 +323,28 @@ AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_ return reader; } -AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec) +AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec) { - AudioReaderPtr reader; - - if (ec) - ec->clear(); - #if defined(_WIN32) SndfileHandle handle(path.wstring().c_str()); #else SndfileHandle handle(path.c_str()); #endif + return createAudioReaderWithHandle(handle, reverse, ec); +} + +AudioReaderPtr createAudioReaderWithFd(int fd, bool reverse, std::error_code* ec) +{ + SndfileHandle handle(fd, false); + return createAudioReaderWithHandle(handle, reverse, ec); +} + +static AudioReaderPtr createExplicitAudioReaderWithHandle(SndfileHandle handle, AudioReaderType type, std::error_code* ec) +{ + AudioReaderPtr reader; + + if (ec) + ec->clear(); if (!handle) { if (ec) @@ -364,4 +368,20 @@ AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType t return reader; } +AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec) +{ +#if defined(_WIN32) + SndfileHandle handle(path.wstring().c_str()); +#else + SndfileHandle handle(path.c_str()); +#endif + return createExplicitAudioReaderWithHandle(handle, type, ec); +} + +AudioReaderPtr createExplicitAudioReaderWithFd(int fd, AudioReaderType type, std::error_code* ec) +{ + SndfileHandle handle(fd, false); + return createExplicitAudioReaderWithHandle(handle, type, ec); +} + } // namespace sfz diff --git a/src/sfizz/AudioReader.h b/src/sfizz/AudioReader.h index 653abce3..65a199a3 100644 --- a/src/sfizz/AudioReader.h +++ b/src/sfizz/AudioReader.h @@ -9,6 +9,7 @@ #include "ghc/fs_std.hpp" #include #include +#include #if defined(_WIN32) #define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 #include @@ -51,9 +52,19 @@ typedef std::unique_ptr AudioReaderPtr; */ AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec = nullptr); +/** + * @brief Create a file reader of detected type. + */ +AudioReaderPtr createAudioReaderWithFd(int fd, bool reverse, std::error_code* ec = nullptr); + /** * @brief Create a file reader of explicit type. (for testing purposes) */ AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec = nullptr); +/** + * @brief Create a file reader of explicit type. (for testing purposes) + */ +AudioReaderPtr createExplicitAudioReaderWithFd(int fd, AudioReaderType type, std::error_code* ec = nullptr); + } // namespace sfz From 4fadda23de545fcd23828b4ed4d512b1fe076ab4 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 18 Jul 2020 13:33:12 +0200 Subject: [PATCH 007/445] Store the last number of active voices instead of computing it on each call to the getter --- src/sfizz/Synth.cpp | 11 +++-------- src/sfizz/Synth.h | 1 + 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c2c7779c..caf410b1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -627,11 +627,6 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept int sfz::Synth::getNumActiveVoices() const noexcept { - auto activeVoices = 0; - for (const auto& voice : voices) { - if (!voice->isFree()) - activeVoices++; - } return activeVoices; } @@ -711,7 +706,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept return; } - int numActiveVoices { 0 }; + activeVoices = 0; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; tempSpan->fill(0.0f); @@ -730,7 +725,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; - numActiveVoices++; + activeVoices++; renderVoiceToOutputs(*voice, *tempSpan); callbackBreakdown.data += voice->getLastDataDuration(); callbackBreakdown.amplitude += voice->getLastAmplitudeDuration(); @@ -770,7 +765,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept } callbackBreakdown.dispatch = dispatchDuration; - resources.logger.logCallbackTime(callbackBreakdown, numActiveVoices, numFrames); + resources.logger.logCallbackTime(callbackBreakdown, activeVoices, numFrames); // Reset the dispatch counter dispatchDuration = Duration(0); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index bcd79542..a0f6f09a 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -731,6 +731,7 @@ private: float sampleRate { config::defaultSampleRate }; float volume { Default::globalVolume }; int numVoices { config::numVoices }; + int activeVoices { 0 }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; // Distribution used to generate random value for the *rand opcodes From 5d9222c771a4b101480f318023ef6813a56635d4 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 18 Jul 2020 13:33:25 +0200 Subject: [PATCH 008/445] Add a readable parameter about the number of active voices --- lv2/sfizz.c | 40 +++++++++++++++++++++++++++++++++------- lv2/sfizz.ttl.in | 21 +++++++++++++++++++-- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index cdb18992..8fad9716 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -60,14 +60,15 @@ #define SFIZZ_URI "http://sfztools.github.io/sfizz" #define SFIZZ_PREFIX SFIZZ_URI "#" -#define SFIZZ__sfzFile "http://sfztools.github.io/sfizz:sfzfile" -#define SFIZZ__tuningfile "http://sfztools.github.io/sfizz:tuningfile" -#define SFIZZ__numVoices "http://sfztools.github.io/sfizz:numvoices" -#define SFIZZ__preloadSize "http://sfztools.github.io/sfizz:preload_size" -#define SFIZZ__oversampling "http://sfztools.github.io/sfizz:oversampling" +#define SFIZZ__sfzFile SFIZZ_URI ":" "sfzfile" +#define SFIZZ__tuningfile SFIZZ_URI ":" "tuningfile" +#define SFIZZ__numVoices SFIZZ_URI ":" "numvoices" +#define SFIZZ__activeVoices SFIZZ_URI ":" "activevoices" +#define SFIZZ__preloadSize SFIZZ_URI ":" "preload_size" +#define SFIZZ__oversampling SFIZZ_URI ":" "oversampling" // These ones are just for the worker -#define SFIZZ__logStatus "http://sfztools.github.io/sfizz:log_status" -#define SFIZZ__checkModification "http://sfztools.github.io/sfizz:check_modification" +#define SFIZZ__logStatus SFIZZ_URI ":" "log_status" +#define SFIZZ__checkModification SFIZZ_URI ":" "check_modification" #define CHANNEL_MASK 0x0F #define MIDI_CHANNEL(byte) (byte & CHANNEL_MASK) @@ -147,6 +148,7 @@ typedef struct LV2_URID sfizz_oversampling_uri; LV2_URID sfizz_log_status_uri; LV2_URID sfizz_check_modification_uri; + LV2_URID sfizz_active_voices_uri; LV2_URID time_position_uri; // Sfizz related data @@ -224,7 +226,9 @@ sfizz_lv2_map_required_uris(sfizz_plugin_t *self) self->sfizz_preload_size_uri = map->map(map->handle, SFIZZ__preloadSize); self->sfizz_oversampling_uri = map->map(map->handle, SFIZZ__oversampling); self->sfizz_log_status_uri = map->map(map->handle, SFIZZ__logStatus); + self->sfizz_log_status_uri = map->map(map->handle, SFIZZ__logStatus); self->sfizz_check_modification_uri = map->map(map->handle, SFIZZ__checkModification); + self->sfizz_active_voices_uri = map->map(map->handle, SFIZZ__activeVoices); self->time_position_uri = map->map(map->handle, LV2_TIME__Position); } @@ -509,6 +513,23 @@ sfizz_lv2_send_file_path(sfizz_plugin_t *self, LV2_URID urid, const char *path) lv2_atom_forge_pop(&self->forge, &frame); } +static void +sfizz_lv2_send_active_voices(sfizz_plugin_t *self) +{ + LV2_Atom_Forge_Frame frame; + const int active_voices = sfizz_get_num_active_voices(self->synth); + + bool write_ok = + lv2_atom_forge_frame_time(&self->forge, 0) && + lv2_atom_forge_object(&self->forge, &frame, 0, self->patch_set_uri) && + lv2_atom_forge_key(&self->forge, self->patch_property_uri) && + lv2_atom_forge_urid(&self->forge, self->sfizz_active_voices_uri) && + lv2_atom_forge_key(&self->forge, self->patch_value_uri) && + lv2_atom_forge_int(&self->forge, active_voices); + + if (write_ok) + lv2_atom_forge_pop(&self->forge, &frame); +} static void sfizz_lv2_handle_atom_object(sfizz_plugin_t *self, const LV2_Atom_Object *obj) @@ -770,6 +791,10 @@ run(LV2_Handle instance, uint32_t sample_count) { sfizz_lv2_send_file_path(self, self->sfizz_scala_file_uri, self->scala_file_path); } + else if (property->body == self->sfizz_active_voices_uri) + { + // We're sending it anyway, nothing to do + } } else if (obj->body.otype == self->time_position_uri) { @@ -802,6 +827,7 @@ run(LV2_Handle instance, uint32_t sample_count) sfizz_lv2_check_preload_size(self); sfizz_lv2_check_oversampling(self); sfizz_lv2_check_num_voices(self); + sfizz_lv2_send_active_voices(self); // Log the buffer usage self->sample_counter += (int)sample_count; diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 6c661987..e2e835fa 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -35,6 +35,12 @@ midnam:update a lv2:Feature . "Accordage"@fr , "Accordatura"@it . +<@LV2PLUGIN_URI@#status> + a pg:Group ; + lv2:symbol "status" ; + lv2:name "status", + "Status"@fr . + <@LV2PLUGIN_URI@:sfzfile> a lv2:Parameter ; rdfs:label "SFZ file", @@ -50,6 +56,16 @@ midnam:update a lv2:Feature . "File Scala"@it ; rdfs:range atom:Path . +<@LV2PLUGIN_URI@:activevoices> + a lv2:Parameter ; + pg:group <@LV2PLUGIN_URI@#status> ; + rdfs:label "Active voices", + "Voix utilisées"@fr ; + rdfs:range atom:Int ; + lv2:minimum 0 ; + lv2:maximum 256 + . + <@LV2PLUGIN_URI@> a doap:Project, lv2:Plugin, lv2:InstrumentPlugin ; @@ -77,8 +93,9 @@ midnam:update a lv2:Feature . opts:supportedOption param:sampleRate ; opts:supportedOption bufsize:maxBlockLength, bufsize:nominalBlockLength ; - patch:writable <@LV2PLUGIN_URI@:sfzfile> ; - patch:writable <@LV2PLUGIN_URI@:tuningfile> ; + patch:writable <@LV2PLUGIN_URI@:sfzfile> , + <@LV2PLUGIN_URI@:tuningfile> ; + patch:readable <@LV2PLUGIN_URI@:activevoices> ; lv2:port [ a lv2:InputPort, atom:AtomPort ; From c2716c3beab0440395bdd9fa6dcb4d9d8bd231aa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 18 Jul 2020 18:20:56 +0200 Subject: [PATCH 009/445] getNumActiveVoices takes a boolean flag to recompute the number of active voices All tests updated as this was the previous default behavior. --- src/sfizz/Synth.cpp | 13 ++++++++-- src/sfizz/Synth.h | 2 +- tests/FilesT.cpp | 16 ++++++------- tests/PolyphonyT.cpp | 26 ++++++++++---------- tests/SynthT.cpp | 56 ++++++++++++++++++++++---------------------- 5 files changed, 61 insertions(+), 52 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index caf410b1..9a62067a 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -625,9 +625,18 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept return {}; } -int sfz::Synth::getNumActiveVoices() const noexcept +int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept { - return activeVoices; + if (!recompute) + return activeVoices; + + int active { 0 }; + for (auto& voice: voices) { + if (!voice->isFree()) + active++; + } + + return active; } void sfz::Synth::garbageCollect() noexcept diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index a0f6f09a..d7f481af 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -396,7 +396,7 @@ public: * * @return int */ - int getNumActiveVoices() const noexcept; + int getNumActiveVoices(bool recompute = false) const noexcept; /** * @brief Get the total number of voices in the synth (the polyphony) * diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 8d85b23a..73ea4ab0 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -474,7 +474,7 @@ TEST_CASE("[Files] Off by with different delays") synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz"); REQUIRE( synth.getNumRegions() == 4 ); synth.noteOn(0, 63, 63); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); auto group1Voice = synth.getVoiceView(0); REQUIRE( group1Voice->getRegion()->group == 1ul ); REQUIRE( group1Voice->getRegion()->offBy == 2ul ); @@ -490,7 +490,7 @@ TEST_CASE("[Files] Off by with the same delays") synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz"); REQUIRE( synth.getNumRegions() == 4 ); synth.noteOn(0, 63, 63); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); auto group1Voice = synth.getVoiceView(0); REQUIRE( group1Voice->getRegion()->group == 1ul ); REQUIRE( group1Voice->getRegion()->offBy == 2ul ); @@ -505,14 +505,14 @@ TEST_CASE("[Files] Off by with the same notes at the same time") synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz"); REQUIRE( synth.getNumRegions() == 4 ); synth.noteOn(0, 65, 63); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.noteOn(0, 65, 63); - REQUIRE( synth.getNumActiveVoices() == 4 ); + REQUIRE( synth.getNumActiveVoices(true) == 4 ); AudioBuffer buffer { 2, 256 }; synth.renderBlock(buffer); synth.noteOn(0, 65, 63); synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); } TEST_CASE("[Files] Off modes") @@ -522,7 +522,7 @@ TEST_CASE("[Files] Off modes") synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_mode.sfz"); REQUIRE( synth.getNumRegions() == 3 ); synth.noteOn(0, 64, 63); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); const auto* fastVoice = synth.getVoiceView(0)->getRegion()->offMode == SfzOffMode::fast ? synth.getVoiceView(0) : @@ -532,10 +532,10 @@ TEST_CASE("[Files] Off modes") synth.getVoiceView(1) : synth.getVoiceView(0) ; synth.noteOn(100, 63, 63); - REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); AudioBuffer buffer { 2, 256 }; synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); REQUIRE( fastVoice->isFree() ); REQUIRE( !normalVoice->isFree() ); } diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 23716cab..aeac0ebe 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -78,7 +78,7 @@ TEST_CASE("[Polyphony] group polyphony limits") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note + REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note } TEST_CASE("[Polyphony] Hierarchy polyphony limits") @@ -91,7 +91,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 2); + REQUIRE(synth.getNumActiveVoices(true) == 2); } TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") @@ -104,7 +104,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 2); + REQUIRE(synth.getNumActiveVoices(true) == 2); } TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") @@ -118,7 +118,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 2); + REQUIRE(synth.getNumActiveVoices(true) == 2); } TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") @@ -137,7 +137,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") synth.noteOn(0, 66, 64); synth.noteOn(0, 66, 64); synth.noteOn(0, 66, 64); - REQUIRE(synth.getNumActiveVoices() == 5); + REQUIRE(synth.getNumActiveVoices(true) == 5); } TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") @@ -151,7 +151,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 2); + REQUIRE(synth.getNumActiveVoices(true) == 2); } TEST_CASE("[Polyphony] Polyphony in master") @@ -171,21 +171,21 @@ TEST_CASE("[Polyphony] Polyphony in master") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note + REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note synth.allSoundOff(); synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices() == 0); + REQUIRE(synth.getNumActiveVoices(true) == 0); synth.noteOn(0, 63, 64); synth.noteOn(0, 63, 64); synth.noteOn(0, 63, 64); - REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note + REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note synth.allSoundOff(); synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices() == 0); + REQUIRE(synth.getNumActiveVoices(true) == 0); synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64); - REQUIRE(synth.getNumActiveVoices() == 3); + REQUIRE(synth.getNumActiveVoices(true) == 3); } @@ -198,7 +198,7 @@ TEST_CASE("[Polyphony] Self-masking") synth.noteOn(0, 64, 63); synth.noteOn(0, 64, 62); synth.noteOn(0, 64, 64); - REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); @@ -216,7 +216,7 @@ TEST_CASE("[Polyphony] Not self-masking") synth.noteOn(0, 66, 63); synth.noteOn(0, 66, 62); synth.noteOn(0, 66, 64); - REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index bd6b847b..44a3faaa 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -23,11 +23,11 @@ TEST_CASE("[Synth] Play and check active voices") synth.noteOn(0, 36, 24); synth.noteOn(0, 36, 89); - REQUIRE(synth.getNumActiveVoices() == 2); + REQUIRE(synth.getNumActiveVoices(true) == 2); // Render for a while for (int i = 0; i < 200; ++i) synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices() == 0); + REQUIRE(synth.getNumActiveVoices(true) == 0); } TEST_CASE("[Synth] All sound off") @@ -36,9 +36,9 @@ TEST_CASE("[Synth] All sound off") synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz"); synth.noteOn(0, 36, 24); synth.noteOn(0, 36, 89); - REQUIRE(synth.getNumActiveVoices() == 2); + REQUIRE(synth.getNumActiveVoices(true) == 2); synth.allSoundOff(); - REQUIRE(synth.getNumActiveVoices() == 0); + REQUIRE(synth.getNumActiveVoices(true) == 0); } TEST_CASE("[Synth] Change the number of voice while playing") @@ -51,9 +51,9 @@ TEST_CASE("[Synth] Change the number of voice while playing") synth.noteOn(0, 36, 24); synth.noteOn(0, 36, 89); synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices() == 2); + REQUIRE(synth.getNumActiveVoices(true) == 2); synth.setNumVoices(8); - REQUIRE(synth.getNumActiveVoices() == 0); + REQUIRE(synth.getNumActiveVoices(true) == 0); REQUIRE(synth.getNumVoices() == 8); } @@ -131,15 +131,15 @@ TEST_CASE("[Synth] All notes offs/all sounds off") )"); synth.noteOn(0, 60, 63); synth.noteOn(0, 62, 63); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.cc(0, 120, 63); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); synth.noteOn(0, 62, 63); synth.noteOn(0, 60, 63); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.cc(0, 123, 63); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); } TEST_CASE("[Synth] Reset all controllers") @@ -491,14 +491,14 @@ TEST_CASE("[Synth] sample quality") // default sample quality synth.noteOn(0, 60, 100); - REQUIRE(synth.getNumActiveVoices() == 1); + REQUIRE(synth.getNumActiveVoices(true) == 1); REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == sfz::Default::sampleQuality); synth.allSoundOff(); // default sample quality, freewheeling synth.enableFreeWheeling(); synth.noteOn(0, 60, 100); - REQUIRE(synth.getNumActiveVoices() == 1); + REQUIRE(synth.getNumActiveVoices(true) == 1); REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == sfz::Default::sampleQualityInFreewheelingMode); synth.allSoundOff(); synth.disableFreeWheeling(); @@ -506,7 +506,7 @@ TEST_CASE("[Synth] sample quality") // user-defined sample quality synth.setSampleQuality(sfz::Synth::ProcessLive, 3); synth.noteOn(0, 60, 100); - REQUIRE(synth.getNumActiveVoices() == 1); + REQUIRE(synth.getNumActiveVoices(true) == 1); REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 3); synth.allSoundOff(); @@ -514,21 +514,21 @@ TEST_CASE("[Synth] sample quality") synth.enableFreeWheeling(); synth.setSampleQuality(sfz::Synth::ProcessFreewheeling, 8); synth.noteOn(0, 60, 100); - REQUIRE(synth.getNumActiveVoices() == 1); + REQUIRE(synth.getNumActiveVoices(true) == 1); REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 8); synth.allSoundOff(); synth.disableFreeWheeling(); // region sample quality synth.noteOn(0, 61, 100); - REQUIRE(synth.getNumActiveVoices() == 1); + REQUIRE(synth.getNumActiveVoices(true) == 1); REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 5); synth.allSoundOff(); // region sample quality, freewheeling synth.enableFreeWheeling(); synth.noteOn(0, 61, 100); - REQUIRE(synth.getNumActiveVoices() == 1); + REQUIRE(synth.getNumActiveVoices(true) == 1); REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 5); synth.allSoundOff(); synth.disableFreeWheeling(); @@ -551,7 +551,7 @@ TEST_CASE("[Synth] Sister voices") REQUIRE( synth.getVoiceView(0)->getNextSisterVoice() == synth.getVoiceView(0) ); REQUIRE( synth.getVoiceView(0)->getPreviousSisterVoice() == synth.getVoiceView(0) ); synth.noteOn(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(1)) == 2 ); REQUIRE( synth.getVoiceView(1)->getNextSisterVoice() == synth.getVoiceView(2) ); REQUIRE( synth.getVoiceView(1)->getPreviousSisterVoice() == synth.getVoiceView(2) ); @@ -559,7 +559,7 @@ TEST_CASE("[Synth] Sister voices") REQUIRE( synth.getVoiceView(2)->getNextSisterVoice() == synth.getVoiceView(1) ); REQUIRE( synth.getVoiceView(2)->getPreviousSisterVoice() == synth.getVoiceView(1) ); synth.noteOn(0, 63, 85); - REQUIRE( synth.getNumActiveVoices() == 6 ); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(3)) == 3 ); REQUIRE( synth.getVoiceView(3)->getNextSisterVoice() == synth.getVoiceView(4) ); REQUIRE( synth.getVoiceView(3)->getPreviousSisterVoice() == synth.getVoiceView(5) ); @@ -599,14 +599,14 @@ TEST_CASE("[Synth] Sisters and off-by") group=2 key=63 sample=*saw )"); synth.noteOn(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 2 ); synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.noteOn(0, 63, 85); - REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 ); } @@ -619,7 +619,7 @@ TEST_CASE("[Synth] Release key") synth.noteOn(0, 62, 85); synth.cc(0, 64, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); } TEST_CASE("[Synth] Release") @@ -631,9 +631,9 @@ TEST_CASE("[Synth] Release") synth.noteOn(0, 62, 85); synth.cc(0, 64, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); synth.cc(0, 64, 0); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); } TEST_CASE("[Synth] Release key (Different sustain CC)") @@ -646,7 +646,7 @@ TEST_CASE("[Synth] Release key (Different sustain CC)") synth.noteOn(0, 62, 85); synth.cc(0, 54, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); } TEST_CASE("[Synth] Release (Different sustain CC)") @@ -659,9 +659,9 @@ TEST_CASE("[Synth] Release (Different sustain CC)") synth.noteOn(0, 62, 85); synth.cc(0, 54, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); synth.cc(0, 54, 0); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); } TEST_CASE("[Synth] Sustain threshold default") From aff0fedb387086a71d6bf587165fa5638b7773bf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 13:50:35 +0200 Subject: [PATCH 010/445] Readable LV2 active voices with control port --- lv2/sfizz.c | 31 ++++++------------------------- lv2/sfizz.ttl.in | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 8fad9716..5bafd04b 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -63,7 +63,6 @@ #define SFIZZ__sfzFile SFIZZ_URI ":" "sfzfile" #define SFIZZ__tuningfile SFIZZ_URI ":" "tuningfile" #define SFIZZ__numVoices SFIZZ_URI ":" "numvoices" -#define SFIZZ__activeVoices SFIZZ_URI ":" "activevoices" #define SFIZZ__preloadSize SFIZZ_URI ":" "preload_size" #define SFIZZ__oversampling SFIZZ_URI ":" "oversampling" // These ones are just for the worker @@ -115,6 +114,7 @@ typedef struct const float *scala_root_key_port; const float *tuning_frequency_port; const float *stretch_tuning_port; + float *active_voices_port; // Atom forge LV2_Atom_Forge forge; ///< Forge for writing atoms in run thread @@ -184,6 +184,7 @@ enum SFIZZ_SCALA_ROOT_KEY = 9, SFIZZ_TUNING_FREQUENCY = 10, SFIZZ_STRETCH_TUNING = 11, + SFIZZ_ACTIVE_VOICES = 12, }; static void @@ -228,7 +229,6 @@ sfizz_lv2_map_required_uris(sfizz_plugin_t *self) self->sfizz_log_status_uri = map->map(map->handle, SFIZZ__logStatus); self->sfizz_log_status_uri = map->map(map->handle, SFIZZ__logStatus); self->sfizz_check_modification_uri = map->map(map->handle, SFIZZ__checkModification); - self->sfizz_active_voices_uri = map->map(map->handle, SFIZZ__activeVoices); self->time_position_uri = map->map(map->handle, LV2_TIME__Position); } @@ -276,6 +276,9 @@ connect_port(LV2_Handle instance, case SFIZZ_STRETCH_TUNING: self->stretch_tuning_port = (const float *)data; break; + case SFIZZ_ACTIVE_VOICES: + self->active_voices_port = (float *)data; + break; default: break; } @@ -513,24 +516,6 @@ sfizz_lv2_send_file_path(sfizz_plugin_t *self, LV2_URID urid, const char *path) lv2_atom_forge_pop(&self->forge, &frame); } -static void -sfizz_lv2_send_active_voices(sfizz_plugin_t *self) -{ - LV2_Atom_Forge_Frame frame; - const int active_voices = sfizz_get_num_active_voices(self->synth); - - bool write_ok = - lv2_atom_forge_frame_time(&self->forge, 0) && - lv2_atom_forge_object(&self->forge, &frame, 0, self->patch_set_uri) && - lv2_atom_forge_key(&self->forge, self->patch_property_uri) && - lv2_atom_forge_urid(&self->forge, self->sfizz_active_voices_uri) && - lv2_atom_forge_key(&self->forge, self->patch_value_uri) && - lv2_atom_forge_int(&self->forge, active_voices); - - if (write_ok) - lv2_atom_forge_pop(&self->forge, &frame); -} - static void sfizz_lv2_handle_atom_object(sfizz_plugin_t *self, const LV2_Atom_Object *obj) { @@ -791,10 +776,6 @@ run(LV2_Handle instance, uint32_t sample_count) { sfizz_lv2_send_file_path(self, self->sfizz_scala_file_uri, self->scala_file_path); } - else if (property->body == self->sfizz_active_voices_uri) - { - // We're sending it anyway, nothing to do - } } else if (obj->body.otype == self->time_position_uri) { @@ -827,7 +808,7 @@ run(LV2_Handle instance, uint32_t sample_count) sfizz_lv2_check_preload_size(self); sfizz_lv2_check_oversampling(self); sfizz_lv2_check_num_voices(self); - sfizz_lv2_send_active_voices(self); + *(self->active_voices_port) = sfizz_get_num_active_voices(self->synth); // Log the buffer usage self->sample_counter += (int)sample_count; diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index e2e835fa..582c4a53 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -56,16 +56,6 @@ midnam:update a lv2:Feature . "File Scala"@it ; rdfs:range atom:Path . -<@LV2PLUGIN_URI@:activevoices> - a lv2:Parameter ; - pg:group <@LV2PLUGIN_URI@#status> ; - rdfs:label "Active voices", - "Voix utilisées"@fr ; - rdfs:range atom:Int ; - lv2:minimum 0 ; - lv2:maximum 256 - . - <@LV2PLUGIN_URI@> a doap:Project, lv2:Plugin, lv2:InstrumentPlugin ; @@ -95,7 +85,6 @@ midnam:update a lv2:Feature . patch:writable <@LV2PLUGIN_URI@:sfzfile> , <@LV2PLUGIN_URI@:tuningfile> ; - patch:readable <@LV2PLUGIN_URI@:activevoices> ; lv2:port [ a lv2:InputPort, atom:AtomPort ; @@ -308,4 +297,15 @@ midnam:update a lv2:Feature . lv2:minimum 0.0 ; lv2:maximum 1.0 ; units:unit units:coef + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 12 ; + lv2:symbol "active_voices" ; + lv2:name "Active voices", + "Voix utilisées"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 256 ; ] . From ab6086b6c53e4fc2e084be0470b9ca3f7002978a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 14:08:10 +0200 Subject: [PATCH 011/445] Rebase on develop and fix sustain tests --- tests/SynthT.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 44a3faaa..ccd92b94 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -673,7 +673,7 @@ TEST_CASE("[Synth] Sustain threshold default") synth.noteOn(0, 62, 85); synth.cc(0, 64, 1); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); } TEST_CASE("[Synth] Sustain threshold") @@ -686,12 +686,12 @@ TEST_CASE("[Synth] Sustain threshold") synth.noteOn(0, 62, 85); synth.cc(0, 64, 1); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); synth.noteOn(0, 62, 85); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.noteOn(0, 62, 85); synth.cc(0, 64, 64); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); } From ce43bb39ff90c1b089ca4aa93d7aabd7ad553b0c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 21:23:10 +0200 Subject: [PATCH 012/445] Allow NumericId to be copyable --- src/sfizz/NumericId.h | 21 ++++++++++++++++----- src/sfizz/Synth.cpp | 8 ++++---- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/sfizz/NumericId.h b/src/sfizz/NumericId.h index b2fed501..5abd906f 100644 --- a/src/sfizz/NumericId.h +++ b/src/sfizz/NumericId.h @@ -18,24 +18,35 @@ struct NumericId { constexpr NumericId() = default; explicit constexpr NumericId(int number) - : number(number) + : number_(number) { } constexpr bool valid() const noexcept { - return number != -1; + return number_ != -1; + } + + constexpr int number() const noexcept + { + return number_; + } + + explicit operator bool() const noexcept + { + return valid(); } constexpr bool operator==(NumericId other) const noexcept { - return number == other.number; + return number_ == other.number_; } constexpr bool operator!=(NumericId other) const noexcept { - return number != other.number; + return number_ != other.number_; } - const int number = -1; +private: + int number_ = -1; }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 9a62067a..58669f1d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1188,10 +1188,10 @@ const sfz::Region* sfz::Synth::getRegionById(NumericId id) const noexcep return nullptr; // search a sequence of ordered identifiers with potential gaps - size_t index = static_cast(id.number); + size_t index = static_cast(id.number()); index = std::min(index, size - 1); - while (index > 0 && regions[index]->getId().number > id.number) + while (index > 0 && regions[index]->getId().number() > id.number()) --index; return (regions[index]->getId() == id) ? regions[index].get() : nullptr; @@ -1205,10 +1205,10 @@ const sfz::Voice* sfz::Synth::getVoiceById(NumericId id) const noexcept return nullptr; // search a sequence of ordered identifiers with potential gaps - size_t index = static_cast(id.number); + size_t index = static_cast(id.number()); index = std::min(index, size - 1); - while (index > 0 && voices[index]->getId().number > id.number) + while (index > 0 && voices[index]->getId().number() > id.number()) --index; return (voices[index]->getId() == id) ? voices[index].get() : nullptr; From 45fe825bc0ad4a9735de5b6a8951051d74bc1528 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 23:28:36 +0200 Subject: [PATCH 013/445] Fix a problem of unwanted copy --- src/sfizz/ModifierHelpers.h | 8 ++++---- src/sfizz/Voice.cpp | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sfizz/ModifierHelpers.h b/src/sfizz/ModifierHelpers.h index 5ea57c02..609b16ff 100644 --- a/src/sfizz/ModifierHelpers.h +++ b/src/sfizz/ModifierHelpers.h @@ -271,8 +271,8 @@ void pitchBendEnvelope(const EventVector& events, absl::Span envelope, F& template void linearModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) { - const auto events = resources.midiState.getCCEvents(ccData.cc); - const auto curve = resources.curves.getCurve(ccData.data.curve); + const auto& events = resources.midiState.getCCEvents(ccData.cc); + const auto& curve = resources.curves.getCurve(ccData.data.curve); if (ccData.data.step == 0.0f) { linearEnvelope(events, span, [&ccData, &curve, &lambda](float x) { return lambda(curve.evalNormalized(x) * ccData.data.value); @@ -301,8 +301,8 @@ void linearModifier(const sfz::Resources& resources, absl::Span span, con template void multiplicativeModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) { - const auto events = resources.midiState.getCCEvents(ccData.cc); - const auto curve = resources.curves.getCurve(ccData.data.curve); + const auto& events = resources.midiState.getCCEvents(ccData.cc); + const auto& curve = resources.curves.getCurve(ccData.data.curve); if (ccData.data.step == 0.0f) { multiplicativeEnvelope(events, span, [&ccData, &curve, &lambda](float x) { return lambda(curve.evalNormalized(x) * ccData.data.value); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 6dff257b..771900d1 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -142,7 +142,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, ASSERT(modifierSmoothers[modId].size() >= region->modifiers[modId].size()); forEachWithSmoother(modId, [modId, this](const CCData& mod, Smoother& smoother) { const auto ccValue = resources.midiState.getCCValue(mod.cc); - const auto curve = resources.curves.getCurve(mod.data.curve); + const auto& curve = resources.curves.getCurve(mod.data.curve); const auto finalValue = curve.evalNormalized(ccValue) * mod.data.value; switch (modId) { case Mod::volume: @@ -347,7 +347,7 @@ void sfz::Voice::applyCrossfades(absl::Span modulationSpan) noexcept bool canShortcut = true; for (const auto& mod : region->crossfadeCCInRange) { - const auto events = resources.midiState.getCCEvents(mod.cc); + const auto& events = resources.midiState.getCCEvents(mod.cc); canShortcut &= (events.size() == 1); linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeIn(mod.data, x, xfCurve); @@ -356,7 +356,7 @@ void sfz::Voice::applyCrossfades(absl::Span modulationSpan) noexcept } for (const auto& mod : region->crossfadeCCOutRange) { - const auto events = resources.midiState.getCCEvents(mod.cc); + const auto& events = resources.midiState.getCCEvents(mod.cc); canShortcut &= (events.size() == 1); linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeOut(mod.data, x, xfCurve); From e574aeb592b2026c742d46e9951ef70b8bf9d214 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 21:01:07 +0200 Subject: [PATCH 014/445] Filter shortcut by relative formula --- src/sfizz/Config.h | 4 ++++ src/sfizz/Smoothers.cpp | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 60c2f0a2..03f7f378 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -103,6 +103,10 @@ namespace config { Background file loading */ static constexpr int backgroundLoaderPthreadPriority = 50; // expressed in % + /** + @brief Ratio to target under which smoothing is considered as completed + */ + static constexpr float smoothingShortcutThreshold = 5e-3; } // namespace config } // namespace sfz diff --git a/src/sfizz/Smoothers.cpp b/src/sfizz/Smoothers.cpp index 59fcc5b9..fda61be0 100644 --- a/src/sfizz/Smoothers.cpp +++ b/src/sfizz/Smoothers.cpp @@ -31,7 +31,13 @@ void Smoother::process(absl::Span input, absl::Span output, if (input.size() == 0) return; - if (canShortcut && std::abs(input.front() - current()) < config::virtuallyZero) { + if (canShortcut) { + float in = input.front(); + float rel = std::abs(in - current()) / (std::abs(in) + config::virtuallyZero); + canShortcut = rel < config::smoothingShortcutThreshold; + } + + if (canShortcut) { if (input.data() != output.data()) copy(input, output); From 93bca10a45a776431663106b51dfc3c3174616a2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 14:55:15 +0200 Subject: [PATCH 015/445] Voice accessor without const qualifier --- src/sfizz/Synth.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index d7f481af..c04b13d0 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -210,6 +210,17 @@ public: * @return const Voice* */ const Voice* getVoiceById(NumericId id) const noexcept; + /** + * @brief Find the voice which is associated with the given identifier. + * + * @param id + * @return Voice* + */ + Voice* getVoiceById(NumericId id) noexcept + { + return const_cast( + const_cast(this)->getVoiceById(id)); + } /** * @brief Get a raw view into a specific region. This is mostly used * for testing. From 45583b6dd9a9bdab1ba47d29a01e033371077183 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 20:29:17 +0200 Subject: [PATCH 016/445] Limit on the number of make jobs --- .travis/script_moddevices.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/script_moddevices.sh b/.travis/script_moddevices.sh index d36b982c..649dbb3d 100755 --- a/.travis/script_moddevices.sh +++ b/.travis/script_moddevices.sh @@ -8,4 +8,4 @@ mkdir -p build/${INSTALL_DIR} && cd build buildenv mod-plugin-builder /usr/local/bin/cmake \ -DSFIZZ_SYSTEM_PROCESSOR=armv7-a \ -DCMAKE_BUILD_TYPE=Release -DSFIZZ_JACK=OFF .. -buildenv mod-plugin-builder make -j +buildenv mod-plugin-builder make -j$(nproc) From 4c1de84c0f15f6c1ce3406b9962f798f7786332e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 19:31:52 +0200 Subject: [PATCH 017/445] Replace the callback mutex with a spin-lock --- src/sfizz/Synth.cpp | 32 ++++++++++++++++---------------- src/sfizz/Synth.h | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c2c7779c..59ca5957 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -30,7 +30,7 @@ sfz::Synth::Synth() sfz::Synth::Synth(int numVoices) { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; parser.setListener(this); effectFactory.registerStandardEffectTypes(); effectBuses.reserve(5); // sufficient room for main and fx1-4 @@ -39,7 +39,7 @@ sfz::Synth::Synth(int numVoices) sfz::Synth::~Synth() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -166,7 +166,7 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) void sfz::Synth::clear() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -396,7 +396,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) { clear(); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; std::error_code ec; fs::path realFile = fs::canonical(file, ec); @@ -417,7 +417,7 @@ bool sfz::Synth::loadSfzString(const fs::path& path, absl::string_view text) { clear(); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; parser.parseString(path, text); if (parser.getErrorCount() > 0) return false; @@ -643,7 +643,7 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept { ASSERT(samplesPerBlock < config::maxBlockSize); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; this->samplesPerBlock = samplesPerBlock; for (auto& voice : voices) @@ -659,7 +659,7 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept void sfz::Synth::setSampleRate(float sampleRate) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; this->sampleRate = sampleRate; for (auto& voice : voices) @@ -698,7 +698,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (resources.synthConfig.freeWheeling) resources.filePool.waitForBackgroundLoading(); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -797,7 +797,7 @@ void sfz::Synth::noteOn(int delay, int noteNumber, uint8_t velocity) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.noteOnEvent(delay, noteNumber, normalizedVelocity); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -813,7 +813,7 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -966,7 +966,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.ccEvent(delay, ccNumber, normValue); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -1277,7 +1277,7 @@ int sfz::Synth::getNumVoices() const noexcept void sfz::Synth::setNumVoices(int numVoices) noexcept { ASSERT(numVoices > 0); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (numVoices == this->numVoices) @@ -1325,7 +1325,7 @@ void sfz::Synth::applySettingsPerVoice() void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (factor == oversamplingFactor) @@ -1348,7 +1348,7 @@ sfz::Oversampling sfz::Synth::getOversamplingFactor() const noexcept void sfz::Synth::setPreloadSize(uint32_t preloadSize) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (preloadSize == resources.filePool.getPreloadSize()) @@ -1381,7 +1381,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept { resources.midiState.resetAllControllers(delay); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -1436,7 +1436,7 @@ void sfz::Synth::disableLogging() noexcept void sfz::Synth::allSoundOff() noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index bcd79542..58d335ad 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -17,10 +17,10 @@ #include "AudioSpan.h" #include "parser/Parser.h" #include "VoiceStealing.h" +#include "utility/SpinMutex.h" #include "absl/types/span.h" #include #include -#include #include #include #include @@ -736,7 +736,7 @@ private: // Distribution used to generate random value for the *rand opcodes std::uniform_real_distribution randNoteDistribution { 0, 1 }; - std::mutex callbackGuard; + SpinMutex callbackGuard; // Singletons passed as references to the voices Resources resources; From 220f9acc1cd50ddf1f891f42187eb3712c74f58a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 5 Aug 2020 01:11:45 +0200 Subject: [PATCH 018/445] Add a header missing from the file list --- src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3f56cc1e..a3e8b802 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,7 @@ source_group ("Faust Files" FILES ${FAUST_FILES}) set (SFIZZ_HEADERS sfizz/ADSREnvelope.h sfizz/AudioBuffer.h + sfizz/AudioReader.h sfizz/AudioSpan.h sfizz/Buffer.h sfizz/BufferPool.h From 4bf2a16f05e385325c8f832cb61d33f86b0ab3ad Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 25 Jul 2020 15:08:32 +0200 Subject: [PATCH 019/445] More generic utility to extract audio file metadata --- dpf.mk | 2 +- src/CMakeLists.txt | 4 +- src/sfizz/FileInstrument.cpp | 143 ------------------- src/sfizz/FileInstrument.h | 24 ---- src/sfizz/FileMetadata.cpp | 260 +++++++++++++++++++++++++++++++++++ src/sfizz/FileMetadata.h | 45 ++++++ src/sfizz/FilePool.cpp | 13 +- tests/FileInstrument.cpp | 17 ++- 8 files changed, 326 insertions(+), 182 deletions(-) delete mode 100644 src/sfizz/FileInstrument.cpp delete mode 100644 src/sfizz/FileInstrument.h create mode 100644 src/sfizz/FileMetadata.cpp create mode 100644 src/sfizz/FileMetadata.h diff --git a/dpf.mk b/dpf.mk index 7dd64e45..51e19b50 100644 --- a/dpf.mk +++ b/dpf.mk @@ -77,7 +77,7 @@ SFIZZ_SOURCES = \ src/sfizz/effects/Width.cpp \ src/sfizz/EQPool.cpp \ src/sfizz/FileId.cpp \ - src/sfizz/FileInstrument.cpp \ + src/sfizz/FileMetadata.cpp \ src/sfizz/FilePool.cpp \ src/sfizz/FilterPool.cpp \ src/sfizz/FloatEnvelopes.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a3e8b802..2c963d78 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,7 +44,7 @@ set (SFIZZ_HEADERS sfizz/EQDescription.h sfizz/EQPool.h sfizz/FileId.h - sfizz/FileInstrument.h + sfizz/FileMetadata.h sfizz/FilePool.h sfizz/FilterDescription.h sfizz/FilterPool.h @@ -93,7 +93,7 @@ set (SFIZZ_SOURCES sfizz/Synth.cpp sfizz/FileId.cpp sfizz/FilePool.cpp - sfizz/FileInstrument.cpp + sfizz/FileMetadata.cpp sfizz/AudioReader.cpp sfizz/FilterPool.cpp sfizz/EQPool.cpp diff --git a/src/sfizz/FileInstrument.cpp b/src/sfizz/FileInstrument.cpp deleted file mode 100644 index 8f4885fe..00000000 --- a/src/sfizz/FileInstrument.cpp +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause - -// This code is part of the sfizz library and is licensed under a BSD 2-clause -// license. You should have receive a LICENSE.md file along with the code. -// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz - -#include "FileInstrument.h" -#include "absl/types/span.h" -#include -#include -#include - -namespace sfz { - -// Utility: file cleanup - -struct FILE_deleter { - void operator()(FILE* x) const noexcept { fclose(x); } -}; -typedef std::unique_ptr FILE_u; - -// Utility: binary file IO - -static bool fread_u32le(FILE* stream, uint32_t& value) -{ - uint8_t bytes[4]; - if (fread(bytes, 4, 1, stream) != 1) - return false; - value = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); - return true; -} - -static bool fread_u32be(FILE* stream, uint32_t& value) -{ - uint8_t bytes[4]; - if (fread(bytes, 4, 1, stream) != 1) - return false; - value = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; - return true; -} - -/** - * @brief Extract the instrument data from the RIFF sampler block - * - * @param data sampler block data, except the 8 leading bytes 'smpl' + size - * @param ins destination instrument - */ -static bool extractSamplerChunkInstrument( - absl::Span data, SF_INSTRUMENT& ins) -{ - auto extractU32 = [&data](const uint32_t offset) -> uint32_t { - const uint8_t* bytes = &data[offset]; - if (bytes + 4 > data.end()) - return 0; - return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); - }; - - ins.gain = 1; - ins.basenote = extractU32(0x14 - 8); - ins.detune = static_cast( // Q0,32 semitones to cents - (static_cast(extractU32(0x18 - 8)) * 100) >> 32); - ins.velocity_lo = 0; - ins.velocity_hi = 127; - ins.key_lo = 0; - ins.key_hi = 127; - - const uint32_t numLoops = std::min(16u, extractU32(0x24 - 8)); - ins.loop_count = numLoops; - - for (uint32_t i = 0; i < numLoops; ++i) { - const uint32_t loopOffset = 0x2c - 8 + i * 24; - - switch (extractU32(loopOffset + 0x04)) { - default: - ins.loops[i].mode = SF_LOOP_NONE; - break; - case 0: - ins.loops[i].mode = SF_LOOP_FORWARD; - break; - case 1: - ins.loops[i].mode = SF_LOOP_ALTERNATING; - break; - case 2: - ins.loops[i].mode = SF_LOOP_BACKWARD; - break; - } - - ins.loops[i].start = extractU32(loopOffset + 0x08); - ins.loops[i].end = extractU32(loopOffset + 0x0c) + 1; - ins.loops[i].count = extractU32(loopOffset + 0x14); - } - - return true; -} - -bool FileInstruments::extractFromFlac(const fs::path& path, SF_INSTRUMENT& ins) -{ - memset(&ins, 0, sizeof(SF_INSTRUMENT)); - -#if !defined(_WIN32) - FILE_u stream(fopen(path.c_str(), "rb")); -#else - FILE_u stream(_wfopen(path.wstring().c_str(), L"rb")); -#endif - - char magic[4]; - if (fread(magic, 4, 1, stream.get()) != 1 || memcmp(magic, "fLaC", 4) != 0) - return false; - - uint32_t header = 0; - while (((header >> 31) & 1) != 1) { - if (!fread_u32be(stream.get(), header)) - return false; - - const uint32_t block_type = (header >> 24) & 0x7f; - const uint32_t block_size = header & ((1 << 24) - 1); - - const off_t off_start_block = ftell(stream.get()); - const off_t off_next_block = off_start_block + block_size; - - if (block_type == 2) { // APPLICATION block - char blockId[4]; - char riffId[4]; - uint32_t riffChunkSize; - if (fread(blockId, 4, 1, stream.get()) == 1 && memcmp(blockId, "riff", 4) == 0 && - fread(riffId, 4, 1, stream.get()) == 1 && memcmp(riffId, "smpl", 4) == 0 && - fread_u32le(stream.get(), riffChunkSize) && riffChunkSize <= block_size - 12) - { - std::unique_ptr chunk { new uint8_t[riffChunkSize] }; - if (fread(chunk.get(), riffChunkSize, 1, stream.get()) == 1) - return extractSamplerChunkInstrument( - { chunk.get(), riffChunkSize }, ins); - } - } - - if (fseek(stream.get(), off_next_block, SEEK_SET) != 0) - return false; - } - - return false; -} - -} // namespace sfz diff --git a/src/sfizz/FileInstrument.h b/src/sfizz/FileInstrument.h deleted file mode 100644 index 1c75e988..00000000 --- a/src/sfizz/FileInstrument.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause - -// This code is part of the sfizz library and is licensed under a BSD 2-clause -// license. You should have receive a LICENSE.md file along with the code. -// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz - -#pragma once -#include "ghc/fs_std.hpp" -#include - -namespace sfz { - -class FileInstruments { -public: -/** - * @brief Extract the loop information of a FLAC file, using RIFF foreign data. - * - * This feature lacks support in libsndfile (as of version 1.0.28). - * see https://github.com/erikd/libsndfile/issues/59 - */ -static bool extractFromFlac(const fs::path& path, SF_INSTRUMENT& ins); -}; - -} // namespace sfz diff --git a/src/sfizz/FileMetadata.cpp b/src/sfizz/FileMetadata.cpp new file mode 100644 index 00000000..6af27921 --- /dev/null +++ b/src/sfizz/FileMetadata.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "FileMetadata.h" +#include +#include + +namespace sfz { + +// Utility: file cleanup + +struct FILE_deleter { + void operator()(FILE* x) const noexcept { fclose(x); } +}; +typedef std::unique_ptr FILE_u; + +// Utility: binary file IO + +static bool fread_u32le(FILE* stream, uint32_t& value) +{ + uint8_t bytes[4]; + if (fread(bytes, 4, 1, stream) != 1) + return false; + value = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + return true; +} + +static bool fread_u32be(FILE* stream, uint32_t& value) +{ + uint8_t bytes[4]; + if (fread(bytes, 4, 1, stream) != 1) + return false; + value = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; + return true; +} + +//------------------------------------------------------------------------------ + +struct FileMetadataReader::Impl { + FILE_u stream_; + std::vector riffChunks_; + + bool openFlac(); + bool openRiff(); +}; + +FileMetadataReader::FileMetadataReader() + : impl_(new Impl) +{ + impl_->riffChunks_.reserve(16); +} + +FileMetadataReader::~FileMetadataReader() +{ +} + +bool FileMetadataReader::open(const fs::path& path) +{ + close(); + +#if !defined(_WIN32) + FILE* stream = fopen(path.c_str(), "rb"); +#else + FILE* stream = _wfopen(path.wstring().c_str(), L"rb"); +#endif + + if (!stream) + return false; + + impl_->stream_.reset(stream); + + char magic[4]; + size_t count = fread(magic, 1, sizeof(magic), stream); + + if (count >= 4 && !memcmp(magic, "fLaC", 4)) { + if (!impl_->openFlac()) { + close(); + return false; + } + } + else if (count >= 4 && !memcmp(magic, "RIFF", 4)) { + if (!impl_->openRiff()) { + close(); + return false; + } + } + + return true; +} + +void FileMetadataReader::close() +{ + impl_->stream_.reset(); + impl_->riffChunks_.clear(); +} + +bool FileMetadataReader::Impl::openFlac() +{ + FILE* stream = stream_.get(); + std::vector& riffChunks = riffChunks_; + + if (fseek(stream, 4, SEEK_SET) != 0) + return false; + + uint32_t header = 0; + while (((header >> 31) & 1) != 1) { + if (!fread_u32be(stream, header)) + return false; + + const uint32_t blockType = (header >> 24) & 0x7f; + const uint32_t blockSize = header & ((1 << 24) - 1); + + const off_t offStartBlock = ftell(stream); + const off_t offNextBlock = offStartBlock + blockSize; + + if (blockType == 2) { // APPLICATION block + char blockId[4]; + char riffId[4]; + uint32_t riffChunkSize; + if (fread(blockId, 4, 1, stream) == 1 && memcmp(blockId, "riff", 4) == 0 && + fread(riffId, 4, 1, stream) == 1 && + fread_u32le(stream, riffChunkSize) && riffChunkSize <= blockSize - 12) + { + RiffChunkInfo info; + info.index = riffChunks.size(); + info.fileOffset = ftell(stream); + memcpy(info.id.data(), riffId, 4); + info.length = riffChunkSize; + riffChunks.push_back(info); + } + } + + if (fseek(stream, offNextBlock, SEEK_SET) != 0) + return false; + } + + return true; +} + +bool FileMetadataReader::Impl::openRiff() +{ + FILE* stream = stream_.get(); + std::vector& riffChunks = riffChunks_; + + if (fseek(stream, 12, SEEK_SET) != 0) + return false; + + char riffId[4]; + uint32_t riffChunkSize; + while (fread(riffId, 4, 1, stream) == 1 && fread_u32le(stream, riffChunkSize)) { + RiffChunkInfo info; + info.index = riffChunks.size(); + info.fileOffset = ftell(stream); + memcpy(info.id.data(), riffId, 4); + info.length = riffChunkSize; + riffChunks.push_back(info); + + if (fseek(stream, riffChunkSize, SEEK_CUR) != 0) + return false; + } + + return true; +} + +size_t FileMetadataReader::riffChunkCount() const +{ + return impl_->riffChunks_.size(); +} + +const RiffChunkInfo* FileMetadataReader::riffChunk(size_t index) const +{ + const std::vector& riffChunks = impl_->riffChunks_; + return (index < riffChunks.size()) ? &riffChunks[index] : nullptr; +} + +const RiffChunkInfo* FileMetadataReader::riffChunkById(RiffChunkId id) const +{ + for (const RiffChunkInfo& riff : impl_->riffChunks_) { + if (riff.id == id) + return &riff; + } + return nullptr; +} + +size_t FileMetadataReader::readRiffData(size_t index, void* buffer, size_t count) +{ + const RiffChunkInfo* riff = riffChunk(index); + if (!riff) + return 0; + + count = (count < riff->length) ? count : riff->length; + + FILE* stream = impl_->stream_.get(); + if (fseek(stream, riff->fileOffset, SEEK_SET) != 0) + return 0; + + return fread(buffer, 1, count, stream); +} + +bool FileMetadataReader::extractRiffInstrument(SF_INSTRUMENT& ins) +{ + const RiffChunkInfo* riff = riffChunkById(RiffChunkId{'s', 'm', 'p', 'l'}); + if (!riff) + return 0; + + constexpr uint32_t maxLoops = 16; + constexpr uint32_t maxChunkSize = 9 * 4 + maxLoops * 6 * 4; + + uint8_t data[maxChunkSize]; + uint32_t length = readRiffData(riff->index, data, sizeof(data)); + + auto extractU32 = [&data, length](const uint32_t offset) -> uint32_t { + const uint8_t* bytes = &data[offset]; + if (bytes + 4 > data + length) + return 0; + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + }; + + ins.gain = 1; + ins.basenote = extractU32(0x14 - 8); + ins.detune = static_cast( // Q0,32 semitones to cents + (static_cast(extractU32(0x18 - 8)) * 100) >> 32); + ins.velocity_lo = 0; + ins.velocity_hi = 127; + ins.key_lo = 0; + ins.key_hi = 127; + + const uint32_t numLoops = std::min(maxLoops, extractU32(0x24 - 8)); + ins.loop_count = numLoops; + + for (uint32_t i = 0; i < numLoops; ++i) { + const uint32_t loopOffset = 0x2c - 8 + i * 24; + + switch (extractU32(loopOffset + 0x04)) { + default: + ins.loops[i].mode = SF_LOOP_NONE; + break; + case 0: + ins.loops[i].mode = SF_LOOP_FORWARD; + break; + case 1: + ins.loops[i].mode = SF_LOOP_ALTERNATING; + break; + case 2: + ins.loops[i].mode = SF_LOOP_BACKWARD; + break; + } + + ins.loops[i].start = extractU32(loopOffset + 0x08); + ins.loops[i].end = extractU32(loopOffset + 0x0c) + 1; + ins.loops[i].count = extractU32(loopOffset + 0x14); + } + + return true; +} + +} // namespace sfz diff --git a/src/sfizz/FileMetadata.h b/src/sfizz/FileMetadata.h new file mode 100644 index 00000000..f703bb76 --- /dev/null +++ b/src/sfizz/FileMetadata.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "ghc/fs_std.hpp" +#include +#include +#include +#include + +namespace sfz { + +typedef std::array RiffChunkId; + +struct RiffChunkInfo { + size_t index; + off_t fileOffset; + RiffChunkId id; + uint32_t length; +}; + +class FileMetadataReader { +public: + FileMetadataReader(); + ~FileMetadataReader(); + + bool open(const fs::path& path); + void close(); + + size_t riffChunkCount() const; + const RiffChunkInfo* riffChunk(size_t index) const; + const RiffChunkInfo* riffChunkById(RiffChunkId id) const; + size_t readRiffData(size_t index, void* buffer, size_t count); + + bool extractRiffInstrument(SF_INSTRUMENT& ins); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 57e0e691..ab013a90 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -25,7 +25,7 @@ #include "FilePool.h" #include "AudioReader.h" -#include "FileInstrument.h" +#include "FileMetadata.h" #include "Buffer.h" #include "AudioBuffer.h" #include "AudioSpan.h" @@ -218,11 +218,12 @@ absl::optional sfz::FilePool::getFileInformation(const Fil SF_INSTRUMENT instrumentInfo {}; - const int sndFormat = reader->format(); - if ((sndFormat & SF_FORMAT_TYPEMASK) == SF_FORMAT_FLAC) - sfz::FileInstruments::extractFromFlac(file, instrumentInfo); - else - reader->getInstrument(&instrumentInfo); + if (!reader->getInstrument(&instrumentInfo)) { + // if no instrument, then try extracting from embedded RIFF chunks (flac) + FileMetadataReader reader; + if (reader.open(file)) + reader.extractRiffInstrument(instrumentInfo); + } if (!fileId.isReverse()) { if (instrumentInfo.loop_count > 0) { diff --git a/tests/FileInstrument.cpp b/tests/FileInstrument.cpp index b6e21c3d..98643be2 100644 --- a/tests/FileInstrument.cpp +++ b/tests/FileInstrument.cpp @@ -4,7 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "sfizz/FileInstrument.h" +#include "sfizz/FileMetadata.h" #include "absl/strings/string_view.h" #include #include @@ -49,13 +49,13 @@ static void usage(const char* argv0) stderr, "Usage: %s [-s|-f] \n" " -s: extract the instrument using libsndfile\n" - " -f: extract the instrument using FLAC metadata\n", + " -f: extract the instrument using RIFF metadata\n", argv0); } enum FileMethod { kMethodSndfile, - kMethodFlac, + kMethodRiff, }; int main(int argc, char *argv[]) @@ -71,7 +71,7 @@ int main(int argc, char *argv[]) if (flag == "-s") method = kMethodSndfile; else if (flag == "-f") - method = kMethodFlac; + method = kMethodRiff; else { usage(argv[0]); return 1; @@ -85,8 +85,13 @@ int main(int argc, char *argv[]) SF_INSTRUMENT ins {}; - if (method == kMethodFlac) { - if (!sfz::FileInstruments::extractFromFlac(path, ins)) { + if (method == kMethodRiff) { + sfz::FileMetadataReader reader; + if (!reader.open(path)) { + fprintf(stderr, "Cannot open file\n"); + return 1; + } + if (!reader.extractRiffInstrument(ins)) { fprintf(stderr, "Cannot get instrument\n"); return 1; } From d6b98d531da2d1d6d4adeb1b157344b783e5236a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 25 Jul 2020 21:39:27 +0200 Subject: [PATCH 020/445] Add extraction of wavetable info --- src/sfizz/FileMetadata.cpp | 141 +++++++++++++++++++++++++++++++++++-- src/sfizz/FileMetadata.h | 45 ++++++++++++ 2 files changed, 180 insertions(+), 6 deletions(-) diff --git a/src/sfizz/FileMetadata.cpp b/src/sfizz/FileMetadata.cpp index 6af27921..87379796 100644 --- a/src/sfizz/FileMetadata.cpp +++ b/src/sfizz/FileMetadata.cpp @@ -4,7 +4,16 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +// Note: Based on some format research from Surge synthesizer +// made by Paul Walker and Mario Kruselj +// cf. Surge src/common/WavSupport.cpp + #include "FileMetadata.h" +#include +#include +#include +#include +#include #include #include @@ -19,12 +28,22 @@ typedef std::unique_ptr FILE_u; // Utility: binary file IO +static uint32_t u32le(const uint8_t *bytes) +{ + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); +} + +static uint32_t u32be(const uint8_t *bytes) +{ + return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; +} + static bool fread_u32le(FILE* stream, uint32_t& value) { uint8_t bytes[4]; if (fread(bytes, 4, 1, stream) != 1) return false; - value = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + value = u32le(bytes); return true; } @@ -33,7 +52,7 @@ static bool fread_u32be(FILE* stream, uint32_t& value) uint8_t bytes[4]; if (fread(bytes, 4, 1, stream) != 1) return false; - value = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; + value = u32be(bytes); return true; } @@ -45,6 +64,14 @@ struct FileMetadataReader::Impl { bool openFlac(); bool openRiff(); + + bool extractClmWavetable(WavetableInfo &wt); + bool extractSurgeWavetable(WavetableInfo &wt); + bool extractUheWavetable(WavetableInfo &wt); + + const RiffChunkInfo* riffChunk(size_t index) const; + const RiffChunkInfo* riffChunkById(RiffChunkId id) const; + size_t readRiffData(size_t index, void* buffer, size_t count); }; FileMetadataReader::FileMetadataReader() @@ -172,13 +199,23 @@ size_t FileMetadataReader::riffChunkCount() const const RiffChunkInfo* FileMetadataReader::riffChunk(size_t index) const { - const std::vector& riffChunks = impl_->riffChunks_; + return impl_->riffChunk(index); +} + +const RiffChunkInfo* FileMetadataReader::Impl::riffChunk(size_t index) const +{ + const std::vector& riffChunks = riffChunks_; return (index < riffChunks.size()) ? &riffChunks[index] : nullptr; } const RiffChunkInfo* FileMetadataReader::riffChunkById(RiffChunkId id) const { - for (const RiffChunkInfo& riff : impl_->riffChunks_) { + return impl_->riffChunkById(id); +} + +const RiffChunkInfo* FileMetadataReader::Impl::riffChunkById(RiffChunkId id) const +{ + for (const RiffChunkInfo& riff : riffChunks_) { if (riff.id == id) return &riff; } @@ -186,6 +223,11 @@ const RiffChunkInfo* FileMetadataReader::riffChunkById(RiffChunkId id) const } size_t FileMetadataReader::readRiffData(size_t index, void* buffer, size_t count) +{ + return impl_->readRiffData(index, buffer, count); +} + +size_t FileMetadataReader::Impl::readRiffData(size_t index, void* buffer, size_t count) { const RiffChunkInfo* riff = riffChunk(index); if (!riff) @@ -193,7 +235,7 @@ size_t FileMetadataReader::readRiffData(size_t index, void* buffer, size_t count count = (count < riff->length) ? count : riff->length; - FILE* stream = impl_->stream_.get(); + FILE* stream = stream_.get(); if (fseek(stream, riff->fileOffset, SEEK_SET) != 0) return 0; @@ -204,7 +246,7 @@ bool FileMetadataReader::extractRiffInstrument(SF_INSTRUMENT& ins) { const RiffChunkInfo* riff = riffChunkById(RiffChunkId{'s', 'm', 'p', 'l'}); if (!riff) - return 0; + return false; constexpr uint32_t maxLoops = 16; constexpr uint32_t maxChunkSize = 9 * 4 + maxLoops * 6 * 4; @@ -257,4 +299,91 @@ bool FileMetadataReader::extractRiffInstrument(SF_INSTRUMENT& ins) return true; } +bool FileMetadataReader::extractWavetableInfo(WavetableInfo& wt) +{ + if (impl_->extractClmWavetable(wt)) + return true; + + if (impl_->extractSurgeWavetable(wt)) + return true; + + if (impl_->extractUheWavetable(wt)) + return true; + + // there also exists a method based on cue chunks used in Surge + // files possibly already covered by the Native case + // otherwise do later when I will have a few samples at hand + + return false; +} + +bool FileMetadataReader::Impl::extractClmWavetable(WavetableInfo &wt) +{ + const RiffChunkInfo* clm = riffChunkById(RiffChunkId{'c', 'l', 'm', ' '}); + if (!clm) + return false; + + char data[16] {}; + if (readRiffData(clm->index, data, sizeof(data)) != sizeof(data)) + return false; + + // 0-2 are "" + // 3-6 is the decimal table size written in ASCII (most likely "2048") + // 7 is a space character + // 8-15 are flags as ASCII digit characters (eg. "01000000") + // 16-end "wavetable ()" + + if (!absl::SimpleAtoi(absl::string_view(data + 3, 4), &wt.tableSize)) + return false; + + int cti = static_cast(data[8]); + if (cti >= '0' && cti <= '4') + cti -= '0'; + else + cti = 0; // unknown interpolation + wt.crossTableInterpolation = cti; + + wt.oneShot = false; + + return true; +} + +bool FileMetadataReader::Impl::extractSurgeWavetable(WavetableInfo &wt) +{ + const RiffChunkInfo* srge; + + if ((srge = riffChunkById(RiffChunkId{'s', 'r', 'g', 'e'}))) + wt.oneShot = false; + else if ((srge = riffChunkById(RiffChunkId{'s', 'r', 'g', 'o'}))) + wt.oneShot = true; + else + return false; + + uint8_t data[8]; + if (readRiffData(srge->index, data, sizeof(data)) != sizeof(data)) + return false; + + //const uint32_t version = u32le(data); + wt.tableSize = u32le(data + 4); + + wt.crossTableInterpolation = 0; + + return true; +} + +bool FileMetadataReader::Impl::extractUheWavetable(WavetableInfo &wt) +{ + const RiffChunkInfo* uhwt = riffChunkById(RiffChunkId{'u', 'h', 'W', 'T'}); + if (!uhwt) + return false; + + // u-he Hive: no idea what is inside this one, 2048 assumed + + wt.tableSize = 2048; + wt.crossTableInterpolation = 0; + wt.oneShot = false; + + return true; +} + } // namespace sfz diff --git a/src/sfizz/FileMetadata.h b/src/sfizz/FileMetadata.h index f703bb76..f4e30bd7 100644 --- a/src/sfizz/FileMetadata.h +++ b/src/sfizz/FileMetadata.h @@ -22,21 +22,66 @@ struct RiffChunkInfo { uint32_t length; }; +struct WavetableInfo { + /** + @brief Size of each successive table in the file + */ + uint32_t tableSize; + /** + * @brief Mode of interpolation between multiple tables + * + * 0: none, 1: crossfade, 2: spectral, + * 3: spectral with fundamental phase set to zero + * 4: spectral with all phases set to zero + */ + int crossTableInterpolation; + /** + * @brief Whether the wavetable is one-shot (does not cycle) + */ + bool oneShot; +}; + class FileMetadataReader { public: FileMetadataReader(); ~FileMetadataReader(); + /** + * @brief Open an audio file of supported format and read internal structures + */ bool open(const fs::path& path); + /** + * @brief Close an audio file + */ void close(); + /** + * @brief Get the number of RIFF chunks in the file + */ size_t riffChunkCount() const; + /** + * @brief Get the information regarding the n-th RIFF chunk + */ const RiffChunkInfo* riffChunk(size_t index) const; + /** + * @brief Get the information regarding the RIFF chunk of given identifier + */ const RiffChunkInfo* riffChunkById(RiffChunkId id) const; + /** + * @brief Read the RIFF data up to the size given (header not included) + */ size_t readRiffData(size_t index, void* buffer, size_t count); + /** + * @brief Extract the RIFF 'smpl' data and convert it to sndfile instrument + */ bool extractRiffInstrument(SF_INSTRUMENT& ins); + /** + * @brief Extract the wavetable information from various relevant RIFF chunks + */ + bool extractWavetableInfo(WavetableInfo& wt); + private: struct Impl; std::unique_ptr impl_; From 8de4452e1518cf7b298f618dd75862c59b02a370 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 5 Aug 2020 04:20:20 +0200 Subject: [PATCH 021/445] Add compressor --- dpf.mk | 1 + scripts/generate_compressor.sh | 54 +++++++ src/CMakeLists.txt | 7 +- src/sfizz/Effects.cpp | 2 + src/sfizz/effects/Compressor.cpp | 204 +++++++++++++++++++++++++++ src/sfizz/effects/Compressor.h | 56 ++++++++ src/sfizz/effects/dsp/compressor.dsp | 11 ++ src/sfizz/effects/gen/compressor.cxx | 180 +++++++++++++++++++++++ 8 files changed, 514 insertions(+), 1 deletion(-) create mode 100755 scripts/generate_compressor.sh create mode 100644 src/sfizz/effects/Compressor.cpp create mode 100644 src/sfizz/effects/Compressor.h create mode 100644 src/sfizz/effects/dsp/compressor.dsp create mode 100644 src/sfizz/effects/gen/compressor.cxx diff --git a/dpf.mk b/dpf.mk index 7dd64e45..77482e90 100644 --- a/dpf.mk +++ b/dpf.mk @@ -60,6 +60,7 @@ SFIZZ_SOURCES = \ src/sfizz/Curve.cpp \ src/sfizz/effects/Apan.cpp \ src/sfizz/Effects.cpp \ + src/sfizz/effects/Compressor.cpp \ src/sfizz/effects/Eq.cpp \ src/sfizz/effects/Filter.cpp \ src/sfizz/effects/Gain.cpp \ diff --git a/scripts/generate_compressor.sh b/scripts/generate_compressor.sh new file mode 100755 index 00000000..7d799a5e --- /dev/null +++ b/scripts/generate_compressor.sh @@ -0,0 +1,54 @@ +#!/bin/sh +set -e + +if ! test -d "src"; then + echo "Please run this in the project root directory." + exit 1 +fi + +# Note: needs faust >= 2.27.1 for UI macros +FAUSTARGS="-uim -inpl" + +# support GNU sed only, use gsed on a Mac +test -z "$SED" && SED=sed + +faustgen() { + mkdir -p src/sfizz/effects/gen + local outfile=src/sfizz/effects/gen/compressor.cxx + + local code=`faust $FAUSTARGS -cn faustCompressor src/sfizz/effects/dsp/compressor.dsp` + + # suppress some faust-specific stuff we don't care + echo "$code" \ + | fgrep -v -- '->declare(' \ + | fgrep -v -- '->openHorizontalBox(' \ + | fgrep -v -- '->openVerticalBox(' \ + | fgrep -v -- '->closeBox(' \ + | fgrep -v -- '->addHorizontalSlider(' \ + | fgrep -v -- '->addVerticalSlider(' \ + > "$outfile" + + # remove metadata + $SED -r -i 's/void[ \t]+metadata[ \t]*\(Meta[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void metadata()/' "$outfile" + + # remove UI + $SED -r -i 's/void[ \t]+buildUserInterface[ \t]*\(UI[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void buildUserInterface()/' "$outfile" + + # remove inheritance + $SED -r -i 's/:[ \t]*public[ \t]+dsp\b\s*//' "$outfile" + + # remove virtual + $SED -r -i 's/\bvirtual\b\s*//' "$outfile" + + # remove undesired UIM + $SED -r -i '/^[ \t]*#define[ \t]+FAUST_(FILE_NAME|CLASS_NAME|INPUTS|OUTPUTS|ACTIVES|PASSIVES)/d' "$outfile" + $SED -r -i '/^[ \t]*FAUST_ADD.*/d' "$outfile" + + # direct access to parameter variables + $SED -r -i 's/\bprivate:/public:/' "$outfile" + + # remove trailing whitespace + $SED -r -i 's/[ \t]+$//' "$outfile" +} + +faustgen diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a3e8b802..c7dea026 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,7 +7,10 @@ set (FAUST_FILES sfizz/dsp/filters/filters_modulable.dsp sfizz/dsp/filters/rbj_filters.dsp sfizz/dsp/filters/sallenkey_modulable.dsp - sfizz/dsp/filters/sfz_filters.dsp) + sfizz/dsp/filters/sfz_filters.dsp + sfizz/effects/dsp/limiter.dsp + sfizz/effects/dsp/resonant_string.dsp + sfizz/effects/dsp/compressor.dsp) source_group ("Faust Files" FILES ${FAUST_FILES}) set (SFIZZ_HEADERS @@ -30,6 +33,7 @@ set (SFIZZ_HEADERS sfizz/effects/Apan.h sfizz/effects/CommonLFO.h sfizz/effects/CommonLFO.hpp + sfizz/effects/Compressor.h sfizz/effects/Eq.h sfizz/effects/Filter.h sfizz/effects/Gain.h @@ -122,6 +126,7 @@ set (SFIZZ_SOURCES sfizz/effects/Apan.cpp sfizz/effects/Lofi.cpp sfizz/effects/Limiter.cpp + sfizz/effects/Compressor.cpp sfizz/effects/Strings.cpp sfizz/effects/Rectify.cpp sfizz/effects/Gain.cpp diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index 26ee10ae..b1ddd84b 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -15,6 +15,7 @@ #include "effects/Apan.h" #include "effects/Lofi.h" #include "effects/Limiter.h" +#include "effects/Compressor.h" #include "effects/Strings.h" #include "effects/Rectify.h" #include "effects/Gain.h" @@ -31,6 +32,7 @@ void EffectFactory::registerStandardEffectTypes() registerEffectType("apan", fx::Apan::makeInstance); registerEffectType("lofi", fx::Lofi::makeInstance); registerEffectType("limiter", fx::Limiter::makeInstance); + registerEffectType("comp", fx::Compressor::makeInstance); registerEffectType("strings", fx::Strings::makeInstance); // extensions (book) diff --git a/src/sfizz/effects/Compressor.cpp b/src/sfizz/effects/Compressor.cpp new file mode 100644 index 00000000..3aaf04a3 --- /dev/null +++ b/src/sfizz/effects/Compressor.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +/* + Note(jpc): implementation status + +- [x] comp_gain Gain (dB) +- [x] comp_attack Attack time (s) +- [x] comp_release Release time (s) +- [x] comp_ratio Ratio (linear gain) +- [x] comp_threshold Threshold (dB) +- [x] comp_stlink Stereo link (boolean) + +*/ + +#include "Compressor.h" +#include "Opcode.h" +#include "AudioSpan.h" +#include "MathHelpers.h" +#include "absl/memory/memory.h" + +static constexpr int _oversampling = 2; +#define FAUST_UIMACROS 1 +#include "gen/compressor.cxx" + +namespace sfz { +namespace fx { + + struct Compressor::Impl { + faustCompressor _compressor[2]; + bool _stlink = false; + float _inputGain = 1.0; + AudioBuffer _tempBuffer2x { 2, _oversampling * config::defaultSamplesPerBlock }; + AudioBuffer _gain2x { 2, _oversampling * config::defaultSamplesPerBlock }; + hiir::Downsampler2xFpu<12> _downsampler2x[EffectChannels]; + hiir::Upsampler2xFpu<12> _upsampler2x[EffectChannels]; + + #define DEFINE_SET_GET(type, ident, name, var, def, min, max, step) \ + float get_##ident(size_t i) const noexcept { return _compressor[i].var; } \ + void set_##ident(size_t i, float value) noexcept { _compressor[i].var = value; } + FAUST_LIST_ACTIVES(DEFINE_SET_GET); + #undef DEFINE_SET_GET + }; + + Compressor::Compressor() + : _impl(new Impl) + { + Impl& impl = *_impl; + for (faustCompressor& comp : impl._compressor) + comp.instanceResetUserInterface(); + } + + Compressor::~Compressor() + { + } + + void Compressor::setSampleRate(double sampleRate) + { + Impl& impl = *_impl; + for (faustCompressor& comp : impl._compressor) { + comp.classInit(sampleRate); + comp.instanceConstants(sampleRate); + } + + static constexpr double coefs2x[12] = { 0.036681502163648017, 0.13654762463195794, 0.27463175937945444, 0.42313861743656711, 0.56109869787919531, 0.67754004997416184, 0.76974183386322703, 0.83988962484963892, 0.89226081800387902, 0.9315419599631839, 0.96209454837808417, 0.98781637073289585 }; + + for (unsigned c = 0; c < EffectChannels; ++c) { + impl._downsampler2x[c].set_coefs(coefs2x); + impl._upsampler2x[c].set_coefs(coefs2x); + } + + clear(); + } + + void Compressor::setSamplesPerBlock(int samplesPerBlock) + { + Impl& impl = *_impl; + impl._tempBuffer2x.resize(_oversampling * samplesPerBlock); + impl._gain2x.resize(_oversampling * samplesPerBlock); + } + + void Compressor::clear() + { + Impl& impl = *_impl; + for (faustCompressor& comp : impl._compressor) + comp.instanceClear(); + } + + void Compressor::process(const float* const inputs[], float* const outputs[], unsigned nframes) + { + Impl& impl = *_impl; + auto inOut2x = AudioSpan(impl._tempBuffer2x).first(_oversampling * nframes); + + absl::Span left2x = inOut2x.getSpan(0); + absl::Span right2x = inOut2x.getSpan(1); + + impl._upsampler2x[0].process_block(left2x.data(), inputs[0], nframes); + impl._upsampler2x[1].process_block(right2x.data(), inputs[1], nframes); + + const float inputGain = impl._inputGain; + for (unsigned i = 0; i < _oversampling * nframes; ++i) { + left2x[i] *= inputGain; + right2x[i] *= inputGain; + } + + if (!impl._stlink) { + absl::Span leftGain2x = impl._gain2x.getSpan(0); + absl::Span rightGain2x = impl._gain2x.getSpan(1); + + { + faustCompressor& comp = impl._compressor[0]; + float* inputs[] = { left2x.data() }; + float* outputs[] = { leftGain2x.data() }; + comp.compute(_oversampling * nframes, inputs, outputs); + } + + { + faustCompressor& comp = impl._compressor[1]; + float* inputs[] = { right2x.data() }; + float* outputs[] = { rightGain2x.data() }; + comp.compute(_oversampling * nframes, inputs, outputs); + } + + for (unsigned i = 0; i < _oversampling * nframes; ++i) { + left2x[i] *= leftGain2x[i]; + right2x[i] *= rightGain2x[i]; + } + } + else { + absl::Span compIn2x = impl._gain2x.getSpan(0); + for (unsigned i = 0; i < _oversampling * nframes; ++i) + compIn2x[i] = std::abs(left2x[i]) + std::abs(right2x[1]); + + absl::Span gain2x = impl._gain2x.getSpan(1); + + { + faustCompressor& comp = impl._compressor[0]; + float* inputs[] = { compIn2x.data() }; + float* outputs[] = { gain2x.data() }; + comp.compute(_oversampling * nframes, inputs, outputs); + } + + for (unsigned i = 0; i < _oversampling * nframes; ++i) { + left2x[i] *= gain2x[i]; + right2x[i] *= gain2x[i]; + } + } + + impl._downsampler2x[0].process_block(outputs[0], left2x.data(), nframes); + impl._downsampler2x[1].process_block(outputs[1], right2x.data(), nframes); + } + + std::unique_ptr Compressor::makeInstance(absl::Span members) + { + Compressor* compressor = new Compressor; + std::unique_ptr fx { compressor }; + + Impl& impl = *compressor->_impl; + + for (const Opcode& opc : members) { + switch (opc.lettersOnlyHash) { + case hash("comp_attack"): + if (auto value = readOpcode(opc.value, {0.0, 10.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Attack(c, *value); + } + break; + case hash("comp_release"): + if (auto value = readOpcode(opc.value, {0.0, 10.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Release(c, *value); + } + break; + case hash("comp_threshold"): + if (auto value = readOpcode(opc.value, {-100.0, 0.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Threshold(c, *value); + } + break; + case hash("comp_ratio"): + if (auto value = readOpcode(opc.value, {1.0, 50.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Ratio(c, *value); + } + break; + case hash("comp_gain"): + if (auto value = readOpcode(opc.value, {-100.0, 100.0})) + impl._inputGain = db2mag(*value); + break; + case hash("comp_stlink"): + if (auto value = readBooleanFromOpcode(opc)) + impl._stlink = *value; + break; + } + } + + return fx; + } + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/Compressor.h b/src/sfizz/effects/Compressor.h new file mode 100644 index 00000000..ed2b0e5d --- /dev/null +++ b/src/sfizz/effects/Compressor.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Effects.h" +#include "hiir/Downsampler2xFpu.h" +#include "hiir/Upsampler2xFpu.h" +#include + +namespace sfz { +namespace fx { + + /** + * @brief Compressor effect + */ + class Compressor : public Effect { + public: + Compressor(); + ~Compressor(); + + /** + * @brief Initializes with the given sample rate. + */ + void setSampleRate(double sampleRate) override; + + /** + * @brief Sets the maximum number of frames to render at a time. The actual + * value can be lower but should never be higher. + */ + void setSamplesPerBlock(int samplesPerBlock) override; + + /** + * @brief Reset the state to initial. + */ + void clear() override; + + /** + * @brief Computes a cycle of the effect in stereo. + */ + void process(const float* const inputs[], float* const outputs[], unsigned nframes) override; + + /** + * @brief Instantiates given the contents of the block. + */ + static std::unique_ptr makeInstance(absl::Span members); + + private: + struct Impl; + std::unique_ptr _impl; + }; + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/dsp/compressor.dsp b/src/sfizz/effects/dsp/compressor.dsp new file mode 100644 index 00000000..6675e7c1 --- /dev/null +++ b/src/sfizz/effects/dsp/compressor.dsp @@ -0,0 +1,11 @@ +import("stdfaust.lib"); + +cgain = co.compression_gain_mono(ratio, thresh, att, rel) with { + ratio = hslider("[1] Ratio", 1.0, 1.0, 20.0, 0.01); + thresh = hslider("[2] Threshold [unit:dB]", 0.0, -60.0, 0.0, 0.01); + over = fconstant(int _oversampling, ); + att = hslider("[3] Attack [unit:s]", 0.0, 0.0, 0.5, 1e-3) : *(over); + rel = hslider("[4] Release [unit:s]", 0.0, 0.0, 5.0, 1e-3) : *(over); +}; + +process = cgain; diff --git a/src/sfizz/effects/gen/compressor.cxx b/src/sfizz/effects/gen/compressor.cxx new file mode 100644 index 00000000..6634dab0 --- /dev/null +++ b/src/sfizz/effects/gen/compressor.cxx @@ -0,0 +1,180 @@ +/* ------------------------------------------------------------ +name: "compressor" +Code generated with Faust 2.27.2 (https://faust.grame.fr) +Compilation options: -lang cpp -inpl -scal -ftz 0 +------------------------------------------------------------ */ + +#ifndef __faustCompressor_H__ +#define __faustCompressor_H__ + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +#include +#include +#include + + +#ifndef FAUSTCLASS +#define FAUSTCLASS faustCompressor +#endif + +#ifdef __APPLE__ +#define exp10f __exp10f +#define exp10 __exp10 +#endif + +class faustCompressor { + + public: + + float fConst0; + float fConst1; + FAUSTFLOAT fHslider0; + int fSampleRate; + float fConst2; + FAUSTFLOAT fHslider1; + FAUSTFLOAT fHslider2; + float fRec2[2]; + float fRec1[2]; + FAUSTFLOAT fHslider3; + float fRec0[2]; + + public: + + void metadata() { + } + + int getNumInputs() { + return 1; + } + int getNumOutputs() { + return 1; + } + int getInputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + int getOutputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + + static void classInit(int sample_rate) { + } + + void instanceConstants(int sample_rate) { + fSampleRate = sample_rate; + fConst0 = float(_oversampling); + fConst1 = (0.5f * fConst0); + fConst2 = (1.0f / std::min(192000.0f, std::max(1.0f, float(fSampleRate)))); + } + + void instanceResetUserInterface() { + fHslider0 = FAUSTFLOAT(0.0f); + fHslider1 = FAUSTFLOAT(1.0f); + fHslider2 = FAUSTFLOAT(0.0f); + fHslider3 = FAUSTFLOAT(0.0f); + } + + void instanceClear() { + for (int l0 = 0; (l0 < 2); l0 = (l0 + 1)) { + fRec2[l0] = 0.0f; + } + for (int l1 = 0; (l1 < 2); l1 = (l1 + 1)) { + fRec1[l1] = 0.0f; + } + for (int l2 = 0; (l2 < 2); l2 = (l2 + 1)) { + fRec0[l2] = 0.0f; + } + } + + void init(int sample_rate) { + classInit(sample_rate); + instanceInit(sample_rate); + } + void instanceInit(int sample_rate) { + instanceConstants(sample_rate); + instanceResetUserInterface(); + instanceClear(); + } + + faustCompressor* clone() { + return new faustCompressor(); + } + + int getSampleRate() { + return fSampleRate; + } + + void buildUserInterface() { + } + + void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { + FAUSTFLOAT* input0 = inputs[0]; + FAUSTFLOAT* output0 = outputs[0]; + float fSlow0 = float(fHslider0); + float fSlow1 = (fConst1 * fSlow0); + int iSlow2 = (std::fabs(fSlow1) < 1.1920929e-07f); + float fSlow3 = (iSlow2 ? 0.0f : std::exp((0.0f - (fConst2 / (iSlow2 ? 1.0f : fSlow1))))); + float fSlow4 = ((1.0f / std::max(1.00000001e-07f, float(fHslider1))) + -1.0f); + float fSlow5 = (fConst0 * fSlow0); + int iSlow6 = (std::fabs(fSlow5) < 1.1920929e-07f); + float fSlow7 = (iSlow6 ? 0.0f : std::exp((0.0f - (fConst2 / (iSlow6 ? 1.0f : fSlow5))))); + float fSlow8 = (fConst0 * float(fHslider2)); + int iSlow9 = (std::fabs(fSlow8) < 1.1920929e-07f); + float fSlow10 = (iSlow9 ? 0.0f : std::exp((0.0f - (fConst2 / (iSlow9 ? 1.0f : fSlow8))))); + float fSlow11 = float(fHslider3); + float fSlow12 = (1.0f - fSlow3); + for (int i = 0; (i < count); i = (i + 1)) { + float fTemp0 = float(input0[i]); + float fTemp1 = std::fabs(fTemp0); + float fTemp2 = ((fRec1[1] > fTemp1) ? fSlow10 : fSlow7); + fRec2[0] = ((fRec2[1] * fTemp2) + (fTemp1 * (1.0f - fTemp2))); + fRec1[0] = fRec2[0]; + fRec0[0] = ((fRec0[1] * fSlow3) + (fSlow4 * (std::max(((20.0f * std::log10(fRec1[0])) - fSlow11), 0.0f) * fSlow12))); + output0[i] = FAUSTFLOAT(std::pow(10.0f, (0.0500000007f * fRec0[0]))); + fRec2[1] = fRec2[0]; + fRec1[1] = fRec1[0]; + fRec0[1] = fRec0[0]; + } + } + +}; + +#ifdef FAUST_UIMACROS + + + + #define FAUST_LIST_ACTIVES(p) \ + p(HORIZONTALSLIDER, Ratio, "Ratio", fHslider1, 1.0f, 1.0f, 20.0f, 0.01f) \ + p(HORIZONTALSLIDER, Threshold, "Threshold", fHslider3, 0.0f, -60.0f, 0.0f, 0.01f) \ + p(HORIZONTALSLIDER, Attack, "Attack", fHslider0, 0.0f, 0.0f, 0.5f, 0.001f) \ + p(HORIZONTALSLIDER, Release, "Release", fHslider2, 0.0f, 0.0f, 5.0f, 0.001f) \ + + #define FAUST_LIST_PASSIVES(p) \ + +#endif + +#endif From b70e1941394868ba0edfcc8ee9a0db8d89578754 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 5 Aug 2020 05:27:01 +0200 Subject: [PATCH 022/445] Add gate --- dpf.mk | 1 + scripts/generate_gate.sh | 54 +++++++++ src/CMakeLists.txt | 5 +- src/sfizz/Effects.cpp | 2 + src/sfizz/effects/Gate.cpp | 203 +++++++++++++++++++++++++++++++++ src/sfizz/effects/Gate.h | 56 +++++++++ src/sfizz/effects/dsp/gate.dsp | 11 ++ src/sfizz/effects/gen/gate.cxx | 195 +++++++++++++++++++++++++++++++ 8 files changed, 526 insertions(+), 1 deletion(-) create mode 100755 scripts/generate_gate.sh create mode 100644 src/sfizz/effects/Gate.cpp create mode 100644 src/sfizz/effects/Gate.h create mode 100644 src/sfizz/effects/dsp/gate.dsp create mode 100644 src/sfizz/effects/gen/gate.cxx diff --git a/dpf.mk b/dpf.mk index 77482e90..ba675096 100644 --- a/dpf.mk +++ b/dpf.mk @@ -64,6 +64,7 @@ SFIZZ_SOURCES = \ src/sfizz/effects/Eq.cpp \ src/sfizz/effects/Filter.cpp \ src/sfizz/effects/Gain.cpp \ + src/sfizz/effects/Gate.cpp \ src/sfizz/effects/impl/ResonantArrayAVX.cpp \ src/sfizz/effects/impl/ResonantArray.cpp \ src/sfizz/effects/impl/ResonantArraySSE.cpp \ diff --git a/scripts/generate_gate.sh b/scripts/generate_gate.sh new file mode 100755 index 00000000..6cb32422 --- /dev/null +++ b/scripts/generate_gate.sh @@ -0,0 +1,54 @@ +#!/bin/sh +set -e + +if ! test -d "src"; then + echo "Please run this in the project root directory." + exit 1 +fi + +# Note: needs faust >= 2.27.1 for UI macros +FAUSTARGS="-uim -inpl" + +# support GNU sed only, use gsed on a Mac +test -z "$SED" && SED=sed + +faustgen() { + mkdir -p src/sfizz/effects/gen + local outfile=src/sfizz/effects/gen/gate.cxx + + local code=`faust $FAUSTARGS -cn faustGate src/sfizz/effects/dsp/gate.dsp` + + # suppress some faust-specific stuff we don't care + echo "$code" \ + | fgrep -v -- '->declare(' \ + | fgrep -v -- '->openHorizontalBox(' \ + | fgrep -v -- '->openVerticalBox(' \ + | fgrep -v -- '->closeBox(' \ + | fgrep -v -- '->addHorizontalSlider(' \ + | fgrep -v -- '->addVerticalSlider(' \ + > "$outfile" + + # remove metadata + $SED -r -i 's/void[ \t]+metadata[ \t]*\(Meta[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void metadata()/' "$outfile" + + # remove UI + $SED -r -i 's/void[ \t]+buildUserInterface[ \t]*\(UI[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void buildUserInterface()/' "$outfile" + + # remove inheritance + $SED -r -i 's/:[ \t]*public[ \t]+dsp\b\s*//' "$outfile" + + # remove virtual + $SED -r -i 's/\bvirtual\b\s*//' "$outfile" + + # remove undesired UIM + $SED -r -i '/^[ \t]*#define[ \t]+FAUST_(FILE_NAME|CLASS_NAME|INPUTS|OUTPUTS|ACTIVES|PASSIVES)/d' "$outfile" + $SED -r -i '/^[ \t]*FAUST_ADD.*/d' "$outfile" + + # direct access to parameter variables + $SED -r -i 's/\bprivate:/public:/' "$outfile" + + # remove trailing whitespace + $SED -r -i 's/[ \t]+$//' "$outfile" +} + +faustgen diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c7dea026..b8ec8e0c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,7 +10,8 @@ set (FAUST_FILES sfizz/dsp/filters/sfz_filters.dsp sfizz/effects/dsp/limiter.dsp sfizz/effects/dsp/resonant_string.dsp - sfizz/effects/dsp/compressor.dsp) + sfizz/effects/dsp/compressor.dsp + sfizz/effects/dsp/gate.dsp) source_group ("Faust Files" FILES ${FAUST_FILES}) set (SFIZZ_HEADERS @@ -37,6 +38,7 @@ set (SFIZZ_HEADERS sfizz/effects/Eq.h sfizz/effects/Filter.h sfizz/effects/Gain.h + sfizz/effects/Gate.h sfizz/effects/Limiter.h sfizz/effects/Lofi.h sfizz/effects/Nothing.h @@ -127,6 +129,7 @@ set (SFIZZ_SOURCES sfizz/effects/Lofi.cpp sfizz/effects/Limiter.cpp sfizz/effects/Compressor.cpp + sfizz/effects/Gate.cpp sfizz/effects/Strings.cpp sfizz/effects/Rectify.cpp sfizz/effects/Gain.cpp diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index b1ddd84b..f41f29dc 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -16,6 +16,7 @@ #include "effects/Lofi.h" #include "effects/Limiter.h" #include "effects/Compressor.h" +#include "effects/Gate.h" #include "effects/Strings.h" #include "effects/Rectify.h" #include "effects/Gain.h" @@ -33,6 +34,7 @@ void EffectFactory::registerStandardEffectTypes() registerEffectType("lofi", fx::Lofi::makeInstance); registerEffectType("limiter", fx::Limiter::makeInstance); registerEffectType("comp", fx::Compressor::makeInstance); + registerEffectType("gate", fx::Gate::makeInstance); registerEffectType("strings", fx::Strings::makeInstance); // extensions (book) diff --git a/src/sfizz/effects/Gate.cpp b/src/sfizz/effects/Gate.cpp new file mode 100644 index 00000000..0b9c8992 --- /dev/null +++ b/src/sfizz/effects/Gate.cpp @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +/* + Note(jpc): implementation status + +- [x] gate_attack Attack time (s) +- [x] gate_release Release time (s) +- [x] gate_threshold Threshold (dB) +- [x] gate_stlink Stereo link (boolean) +- [ ] gate_onccN Gate manual control (% - 0%=on, 100%=off) + + Sfizz Extra + +- [x] gate_hold Hold time (s) + +*/ + +#include "Gate.h" +#include "Opcode.h" +#include "AudioSpan.h" +#include "MathHelpers.h" +#include "absl/memory/memory.h" + +static constexpr int _oversampling = 2; +#define FAUST_UIMACROS 1 +#include "gen/gate.cxx" + +namespace sfz { +namespace fx { + + struct Gate::Impl { + faustGate _gate[2]; + bool _stlink = false; + float _inputGain = 1.0; + AudioBuffer _tempBuffer2x { 2, _oversampling * config::defaultSamplesPerBlock }; + AudioBuffer _gain2x { 2, _oversampling * config::defaultSamplesPerBlock }; + hiir::Downsampler2xFpu<12> _downsampler2x[EffectChannels]; + hiir::Upsampler2xFpu<12> _upsampler2x[EffectChannels]; + + #define DEFINE_SET_GET(type, ident, name, var, def, min, max, step) \ + float get_##ident(size_t i) const noexcept { return _gate[i].var; } \ + void set_##ident(size_t i, float value) noexcept { _gate[i].var = value; } + FAUST_LIST_ACTIVES(DEFINE_SET_GET); + #undef DEFINE_SET_GET + }; + + Gate::Gate() + : _impl(new Impl) + { + Impl& impl = *_impl; + for (faustGate& gate : impl._gate) + gate.instanceResetUserInterface(); + } + + Gate::~Gate() + { + } + + void Gate::setSampleRate(double sampleRate) + { + Impl& impl = *_impl; + for (faustGate& gate : impl._gate) { + gate.classInit(sampleRate); + gate.instanceConstants(sampleRate); + } + + static constexpr double coefs2x[12] = { 0.036681502163648017, 0.13654762463195794, 0.27463175937945444, 0.42313861743656711, 0.56109869787919531, 0.67754004997416184, 0.76974183386322703, 0.83988962484963892, 0.89226081800387902, 0.9315419599631839, 0.96209454837808417, 0.98781637073289585 }; + + for (unsigned c = 0; c < EffectChannels; ++c) { + impl._downsampler2x[c].set_coefs(coefs2x); + impl._upsampler2x[c].set_coefs(coefs2x); + } + + clear(); + } + + void Gate::setSamplesPerBlock(int samplesPerBlock) + { + Impl& impl = *_impl; + impl._tempBuffer2x.resize(_oversampling * samplesPerBlock); + impl._gain2x.resize(_oversampling * samplesPerBlock); + } + + void Gate::clear() + { + Impl& impl = *_impl; + for (faustGate& gate : impl._gate) + gate.instanceClear(); + } + + void Gate::process(const float* const inputs[], float* const outputs[], unsigned nframes) + { + Impl& impl = *_impl; + auto inOut2x = AudioSpan(impl._tempBuffer2x).first(_oversampling * nframes); + + absl::Span left2x = inOut2x.getSpan(0); + absl::Span right2x = inOut2x.getSpan(1); + + impl._upsampler2x[0].process_block(left2x.data(), inputs[0], nframes); + impl._upsampler2x[1].process_block(right2x.data(), inputs[1], nframes); + + const float inputGain = impl._inputGain; + for (unsigned i = 0; i < _oversampling * nframes; ++i) { + left2x[i] *= inputGain; + right2x[i] *= inputGain; + } + + if (!impl._stlink) { + absl::Span leftGain2x = impl._gain2x.getSpan(0); + absl::Span rightGain2x = impl._gain2x.getSpan(1); + + { + faustGate& gate = impl._gate[0]; + float* inputs[] = { left2x.data() }; + float* outputs[] = { leftGain2x.data() }; + gate.compute(_oversampling * nframes, inputs, outputs); + } + + { + faustGate& gate = impl._gate[1]; + float* inputs[] = { right2x.data() }; + float* outputs[] = { rightGain2x.data() }; + gate.compute(_oversampling * nframes, inputs, outputs); + } + + for (unsigned i = 0; i < _oversampling * nframes; ++i) { + left2x[i] *= leftGain2x[i]; + right2x[i] *= rightGain2x[i]; + } + } + else { + absl::Span gateIn2x = impl._gain2x.getSpan(0); + for (unsigned i = 0; i < _oversampling * nframes; ++i) + gateIn2x[i] = std::abs(left2x[i]) + std::abs(right2x[1]); + + absl::Span gain2x = impl._gain2x.getSpan(1); + + { + faustGate& gate = impl._gate[0]; + float* inputs[] = { gateIn2x.data() }; + float* outputs[] = { gain2x.data() }; + gate.compute(_oversampling * nframes, inputs, outputs); + } + + for (unsigned i = 0; i < _oversampling * nframes; ++i) { + left2x[i] *= gain2x[i]; + right2x[i] *= gain2x[i]; + } + } + + impl._downsampler2x[0].process_block(outputs[0], left2x.data(), nframes); + impl._downsampler2x[1].process_block(outputs[1], right2x.data(), nframes); + } + + std::unique_ptr Gate::makeInstance(absl::Span members) + { + Gate* gate = new Gate; + std::unique_ptr fx { gate }; + + Impl& impl = *gate->_impl; + + for (const Opcode& opc : members) { + switch (opc.lettersOnlyHash) { + case hash("gate_attack"): + if (auto value = readOpcode(opc.value, {0.0, 10.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Attack(c, *value); + } + break; + case hash("gate_hold"): + if (auto value = readOpcode(opc.value, {0.0, 10.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Hold(c, *value); + } + break; + case hash("gate_release"): + if (auto value = readOpcode(opc.value, {0.0, 10.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Release(c, *value); + } + break; + case hash("gate_threshold"): + if (auto value = readOpcode(opc.value, {-100.0, 0.0})) { + for (size_t c = 0; c < 2; ++c) + impl.set_Threshold(c, *value); + } + break; + case hash("gate_stlink"): + if (auto value = readBooleanFromOpcode(opc)) + impl._stlink = *value; + break; + } + } + + return fx; + } + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/Gate.h b/src/sfizz/effects/Gate.h new file mode 100644 index 00000000..bfe3ccfd --- /dev/null +++ b/src/sfizz/effects/Gate.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Effects.h" +#include "hiir/Downsampler2xFpu.h" +#include "hiir/Upsampler2xFpu.h" +#include + +namespace sfz { +namespace fx { + + /** + * @brief Gate effect + */ + class Gate : public Effect { + public: + Gate(); + ~Gate(); + + /** + * @brief Initializes with the given sample rate. + */ + void setSampleRate(double sampleRate) override; + + /** + * @brief Sets the maximum number of frames to render at a time. The actual + * value can be lower but should never be higher. + */ + void setSamplesPerBlock(int samplesPerBlock) override; + + /** + * @brief Reset the state to initial. + */ + void clear() override; + + /** + * @brief Computes a cycle of the effect in stereo. + */ + void process(const float* const inputs[], float* const outputs[], unsigned nframes) override; + + /** + * @brief Instantiates given the contents of the block. + */ + static std::unique_ptr makeInstance(absl::Span members); + + private: + struct Impl; + std::unique_ptr _impl; + }; + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/dsp/gate.dsp b/src/sfizz/effects/dsp/gate.dsp new file mode 100644 index 00000000..7bac7f27 --- /dev/null +++ b/src/sfizz/effects/dsp/gate.dsp @@ -0,0 +1,11 @@ +import("stdfaust.lib"); + +ggain = ef.gate_gain_mono(thresh, att, hold, rel) with { + thresh = hslider("[1] Threshold [unit:dB]", 0.0, -60.0, 0.0, 0.01); + over = fconstant(int _oversampling, ); + att = hslider("[2] Attack [unit:s]", 0.0, 0.0, 10.0, 1e-3) : *(over); + hold = hslider("[3] Hold [unit:s]", 0.0, 0.0, 10.0, 1e-3) : *(over); + rel = hslider("[4] Release [unit:s]", 0.0, 0.0, 5.0, 1e-3) : *(over); +}; + +process = ggain; diff --git a/src/sfizz/effects/gen/gate.cxx b/src/sfizz/effects/gen/gate.cxx new file mode 100644 index 00000000..099eb3c5 --- /dev/null +++ b/src/sfizz/effects/gen/gate.cxx @@ -0,0 +1,195 @@ +/* ------------------------------------------------------------ +name: "gate" +Code generated with Faust 2.27.2 (https://faust.grame.fr) +Compilation options: -lang cpp -inpl -scal -ftz 0 +------------------------------------------------------------ */ + +#ifndef __faustGate_H__ +#define __faustGate_H__ + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +#include +#include +#include + + +#ifndef FAUSTCLASS +#define FAUSTCLASS faustGate +#endif + +#ifdef __APPLE__ +#define exp10f __exp10f +#define exp10 __exp10 +#endif + +class faustGate { + + public: + + float fConst0; + FAUSTFLOAT fHslider0; + FAUSTFLOAT fHslider1; + int fSampleRate; + float fConst1; + float fConst2; + float fRec3[2]; + FAUSTFLOAT fHslider2; + int iVec0[2]; + float fConst3; + FAUSTFLOAT fHslider3; + int iRec4[2]; + float fRec1[2]; + float fRec0[2]; + + public: + + void metadata() { + } + + int getNumInputs() { + return 1; + } + int getNumOutputs() { + return 1; + } + int getInputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + int getOutputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + + static void classInit(int sample_rate) { + } + + void instanceConstants(int sample_rate) { + fSampleRate = sample_rate; + fConst0 = float(_oversampling); + fConst1 = std::min(192000.0f, std::max(1.0f, float(fSampleRate))); + fConst2 = (1.0f / fConst1); + fConst3 = (fConst1 * fConst0); + } + + void instanceResetUserInterface() { + fHslider0 = FAUSTFLOAT(0.0f); + fHslider1 = FAUSTFLOAT(0.0f); + fHslider2 = FAUSTFLOAT(0.0f); + fHslider3 = FAUSTFLOAT(0.0f); + } + + void instanceClear() { + for (int l0 = 0; (l0 < 2); l0 = (l0 + 1)) { + fRec3[l0] = 0.0f; + } + for (int l1 = 0; (l1 < 2); l1 = (l1 + 1)) { + iVec0[l1] = 0; + } + for (int l2 = 0; (l2 < 2); l2 = (l2 + 1)) { + iRec4[l2] = 0; + } + for (int l3 = 0; (l3 < 2); l3 = (l3 + 1)) { + fRec1[l3] = 0.0f; + } + for (int l4 = 0; (l4 < 2); l4 = (l4 + 1)) { + fRec0[l4] = 0.0f; + } + } + + void init(int sample_rate) { + classInit(sample_rate); + instanceInit(sample_rate); + } + void instanceInit(int sample_rate) { + instanceConstants(sample_rate); + instanceResetUserInterface(); + instanceClear(); + } + + faustGate* clone() { + return new faustGate(); + } + + int getSampleRate() { + return fSampleRate; + } + + void buildUserInterface() { + } + + void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { + FAUSTFLOAT* input0 = inputs[0]; + FAUSTFLOAT* output0 = outputs[0]; + float fSlow0 = (fConst0 * float(fHslider0)); + float fSlow1 = (fConst0 * float(fHslider1)); + float fSlow2 = std::min(fSlow0, fSlow1); + int iSlow3 = (std::fabs(fSlow2) < 1.1920929e-07f); + float fSlow4 = (iSlow3 ? 0.0f : std::exp((0.0f - (fConst2 / (iSlow3 ? 1.0f : fSlow2))))); + float fSlow5 = (1.0f - fSlow4); + float fSlow6 = std::pow(10.0f, (0.0500000007f * float(fHslider2))); + int iSlow7 = int((fConst3 * float(fHslider3))); + int iSlow8 = (std::fabs(fSlow0) < 1.1920929e-07f); + float fSlow9 = (iSlow8 ? 0.0f : std::exp((0.0f - (fConst2 / (iSlow8 ? 1.0f : fSlow0))))); + int iSlow10 = (std::fabs(fSlow1) < 1.1920929e-07f); + float fSlow11 = (iSlow10 ? 0.0f : std::exp((0.0f - (fConst2 / (iSlow10 ? 1.0f : fSlow1))))); + for (int i = 0; (i < count); i = (i + 1)) { + float fTemp0 = float(input0[i]); + fRec3[0] = ((fRec3[1] * fSlow4) + (std::fabs(fTemp0) * fSlow5)); + float fRec2 = fRec3[0]; + int iTemp1 = (fRec2 > fSlow6); + iVec0[0] = iTemp1; + iRec4[0] = std::max(int((iSlow7 * (iTemp1 < iVec0[1]))), int((iRec4[1] + -1))); + float fTemp2 = std::fabs(std::max(float(iTemp1), float((iRec4[0] > 0)))); + float fTemp3 = ((fRec0[1] > fTemp2) ? fSlow11 : fSlow9); + fRec1[0] = ((fRec1[1] * fTemp3) + (fTemp2 * (1.0f - fTemp3))); + fRec0[0] = fRec1[0]; + output0[i] = FAUSTFLOAT(fRec0[0]); + fRec3[1] = fRec3[0]; + iVec0[1] = iVec0[0]; + iRec4[1] = iRec4[0]; + fRec1[1] = fRec1[0]; + fRec0[1] = fRec0[0]; + } + } + +}; + +#ifdef FAUST_UIMACROS + + + + #define FAUST_LIST_ACTIVES(p) \ + p(HORIZONTALSLIDER, Threshold, "Threshold", fHslider2, 0.0f, -60.0f, 0.0f, 0.01f) \ + p(HORIZONTALSLIDER, Attack, "Attack", fHslider0, 0.0f, 0.0f, 10.0f, 0.001f) \ + p(HORIZONTALSLIDER, Hold, "Hold", fHslider3, 0.0f, 0.0f, 10.0f, 0.001f) \ + p(HORIZONTALSLIDER, Release, "Release", fHslider1, 0.0f, 0.0f, 5.0f, 0.001f) \ + + #define FAUST_LIST_PASSIVES(p) \ + +#endif + +#endif From 91050fd2d815a0c6139ec4b1bb329401e47cb442 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 5 Aug 2020 05:42:02 +0200 Subject: [PATCH 023/445] Clarify a comment --- src/sfizz/effects/Gate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/effects/Gate.cpp b/src/sfizz/effects/Gate.cpp index 0b9c8992..b7a819d5 100644 --- a/src/sfizz/effects/Gate.cpp +++ b/src/sfizz/effects/Gate.cpp @@ -11,7 +11,7 @@ - [x] gate_release Release time (s) - [x] gate_threshold Threshold (dB) - [x] gate_stlink Stereo link (boolean) -- [ ] gate_onccN Gate manual control (% - 0%=on, 100%=off) +- [ ] gate_onccN Gate manual control (% - 0%=gate open, 100%=gate closed) Sfizz Extra From b56821db00dfa0f9dd0875ef77cd617d12a3bdbd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 00:53:43 +0200 Subject: [PATCH 024/445] Add the distortion --- dpf.mk | 1 + scripts/generate_disto.sh | 54 ++++++ src/CMakeLists.txt | 5 +- src/sfizz/Effects.cpp | 2 + src/sfizz/effects/Disto.cpp | 226 +++++++++++++++++++++++ src/sfizz/effects/Disto.h | 54 ++++++ src/sfizz/effects/dsp/disto_stage.dsp | 40 +++++ src/sfizz/effects/gen/disto_stage.cxx | 248 ++++++++++++++++++++++++++ 8 files changed, 629 insertions(+), 1 deletion(-) create mode 100755 scripts/generate_disto.sh create mode 100644 src/sfizz/effects/Disto.cpp create mode 100644 src/sfizz/effects/Disto.h create mode 100644 src/sfizz/effects/dsp/disto_stage.dsp create mode 100644 src/sfizz/effects/gen/disto_stage.cxx diff --git a/dpf.mk b/dpf.mk index ba675096..f79e7d41 100644 --- a/dpf.mk +++ b/dpf.mk @@ -61,6 +61,7 @@ SFIZZ_SOURCES = \ src/sfizz/effects/Apan.cpp \ src/sfizz/Effects.cpp \ src/sfizz/effects/Compressor.cpp \ + src/sfizz/effects/Disto.cpp \ src/sfizz/effects/Eq.cpp \ src/sfizz/effects/Filter.cpp \ src/sfizz/effects/Gain.cpp \ diff --git a/scripts/generate_disto.sh b/scripts/generate_disto.sh new file mode 100755 index 00000000..095cb147 --- /dev/null +++ b/scripts/generate_disto.sh @@ -0,0 +1,54 @@ +#!/bin/sh +set -e + +if ! test -d "src"; then + echo "Please run this in the project root directory." + exit 1 +fi + +# Note: needs faust >= 2.27.1 for UI macros +FAUSTARGS="-uim -inpl" + +# support GNU sed only, use gsed on a Mac +test -z "$SED" && SED=sed + +faustgen() { + mkdir -p src/sfizz/effects/gen + local outfile=src/sfizz/effects/gen/disto_stage.cxx + + local code=`faust $FAUSTARGS -cn faustDisto src/sfizz/effects/dsp/disto_stage.dsp` + + # suppress some faust-specific stuff we don't care + echo "$code" \ + | fgrep -v -- '->declare(' \ + | fgrep -v -- '->openHorizontalBox(' \ + | fgrep -v -- '->openVerticalBox(' \ + | fgrep -v -- '->closeBox(' \ + | fgrep -v -- '->addHorizontalSlider(' \ + | fgrep -v -- '->addVerticalSlider(' \ + > "$outfile" + + # remove metadata + $SED -r -i 's/void[ \t]+metadata[ \t]*\(Meta[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void metadata()/' "$outfile" + + # remove UI + $SED -r -i 's/void[ \t]+buildUserInterface[ \t]*\(UI[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void buildUserInterface()/' "$outfile" + + # remove inheritance + $SED -r -i 's/:[ \t]*public[ \t]+dsp\b\s*//' "$outfile" + + # remove virtual + $SED -r -i 's/\bvirtual\b\s*//' "$outfile" + + # remove undesired UIM + $SED -r -i '/^[ \t]*#define[ \t]+FAUST_(FILE_NAME|CLASS_NAME|INPUTS|OUTPUTS|ACTIVES|PASSIVES)/d' "$outfile" + $SED -r -i '/^[ \t]*FAUST_ADD.*/d' "$outfile" + + # direct access to parameter variables + $SED -r -i 's/\bprivate:/public:/' "$outfile" + + # remove trailing whitespace + $SED -r -i 's/[ \t]+$//' "$outfile" +} + +faustgen diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b8ec8e0c..6d0d5c83 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,7 +11,8 @@ set (FAUST_FILES sfizz/effects/dsp/limiter.dsp sfizz/effects/dsp/resonant_string.dsp sfizz/effects/dsp/compressor.dsp - sfizz/effects/dsp/gate.dsp) + sfizz/effects/dsp/gate.dsp + sfizz/effects/dsp/disto_stage.dsp) source_group ("Faust Files" FILES ${FAUST_FILES}) set (SFIZZ_HEADERS @@ -35,6 +36,7 @@ set (SFIZZ_HEADERS sfizz/effects/CommonLFO.h sfizz/effects/CommonLFO.hpp sfizz/effects/Compressor.h + sfizz/effects/Disto.h sfizz/effects/Eq.h sfizz/effects/Filter.h sfizz/effects/Gain.h @@ -130,6 +132,7 @@ set (SFIZZ_SOURCES sfizz/effects/Limiter.cpp sfizz/effects/Compressor.cpp sfizz/effects/Gate.cpp + sfizz/effects/Disto.cpp sfizz/effects/Strings.cpp sfizz/effects/Rectify.cpp sfizz/effects/Gain.cpp diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index f41f29dc..cc9254b1 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -17,6 +17,7 @@ #include "effects/Limiter.h" #include "effects/Compressor.h" #include "effects/Gate.h" +#include "effects/Disto.h" #include "effects/Strings.h" #include "effects/Rectify.h" #include "effects/Gain.h" @@ -35,6 +36,7 @@ void EffectFactory::registerStandardEffectTypes() registerEffectType("limiter", fx::Limiter::makeInstance); registerEffectType("comp", fx::Compressor::makeInstance); registerEffectType("gate", fx::Gate::makeInstance); + registerEffectType("disto", fx::Disto::makeInstance); registerEffectType("strings", fx::Strings::makeInstance); // extensions (book) diff --git a/src/sfizz/effects/Disto.cpp b/src/sfizz/effects/Disto.cpp new file mode 100644 index 00000000..4aea5188 --- /dev/null +++ b/src/sfizz/effects/Disto.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +/* + Note(jpc): implementation status + +- [x] disto_tone +- [ ] disto_tone_oncc +- [x] disto_depth +- [ ] disto_depth_oncc +- [x] disto_stages +- [x] disto_dry +- [ ] disto_dry_oncc +- [x] disto_wet +- [ ] disto_wet_oncc +*/ + +#include "Disto.h" +#include "Opcode.h" +#include "Config.h" +#include +#include +#include +#include + +static constexpr int _oversampling = 8; +#define FAUST_UIMACROS 1 +#include "gen/disto_stage.cxx" + +namespace sfz { +namespace fx { + +struct Disto::Impl { + enum { maxStages = 4 }; + + float _samplePeriod = 1.0 / config::defaultSampleRate; + float _tone = 100.0; + float _depth = 0.0; + float _dry = 0.0; + float _wet = 0.0; + unsigned _numStages = 1; + + float _toneLpfMem[EffectChannels] = {}; + faustDisto _stages[EffectChannels][maxStages]; + + hiir::Upsampler2xFpu<12> _up2x[EffectChannels]; + hiir::Upsampler2xFpu<4> _up4x[EffectChannels]; + hiir::Upsampler2xFpu<3> _up8x[EffectChannels]; + + hiir::Downsampler2xFpu<12> _down2x[EffectChannels]; + hiir::Downsampler2xFpu<4> _down4x[EffectChannels]; + hiir::Downsampler2xFpu<3> _down8x[EffectChannels]; + + std::unique_ptr _temp8x[2]; + + // use the same formula as reverb + float toneCutoff() const noexcept { return 21.0f + _tone * 1.08f; } + + #define DEFINE_SET_GET(type, ident, name, var, def, min, max, step) \ + float get_##ident(size_t c, size_t s) const noexcept { return _stages[c][s].var; } \ + void set_##ident(size_t c, size_t s, float value) noexcept { _stages[c][s].var = value; } + FAUST_LIST_ACTIVES(DEFINE_SET_GET); + #undef DEFINE_SET_GET +}; + +Disto::Disto() + : _impl(new Impl) +{ + Impl& impl = *_impl; + + for (unsigned c = 0; c < EffectChannels; ++c) { + for (faustDisto& stage : impl._stages[c]) + stage.init(config::defaultSampleRate); + } +} + +Disto::~Disto() +{ +} + +void Disto::setSampleRate(double sampleRate) +{ + Impl& impl = *_impl; + impl._samplePeriod = 1.0 / sampleRate; + + for (unsigned c = 0; c < EffectChannels; ++c) { + for (faustDisto& stage : impl._stages[c]) { + stage.classInit(sampleRate); + stage.instanceConstants(sampleRate); + } + } + + static constexpr double coefs2x[12] = { 0.036681502163648017, 0.13654762463195794, 0.27463175937945444, 0.42313861743656711, 0.56109869787919531, 0.67754004997416184, 0.76974183386322703, 0.83988962484963892, 0.89226081800387902, 0.9315419599631839, 0.96209454837808417, 0.98781637073289585 }; + static constexpr double coefs4x[4] = { 0.042448989488488006, 0.17072114107630679, 0.39329183835224008, 0.74569514831986694 }; + static constexpr double coefs8x[3] = { 0.055748680811302048, 0.24305119574153092, 0.6466991311926823 }; + + for (unsigned c = 0; c < EffectChannels; ++c) { + impl._down2x[c].set_coefs(coefs2x); + impl._down4x[c].set_coefs(coefs4x); + impl._down8x[c].set_coefs(coefs8x); + impl._up2x[c].set_coefs(coefs2x); + impl._up4x[c].set_coefs(coefs4x); + impl._up8x[c].set_coefs(coefs8x); + } +} + +void Disto::setSamplesPerBlock(int samplesPerBlock) +{ + Impl& impl = *_impl; + + for (std::unique_ptr& temp : impl._temp8x) + temp.reset(new float[8 * samplesPerBlock]); +} + +void Disto::clear() +{ + Impl& impl = *_impl; + for (unsigned c = 0; c < EffectChannels; ++c) { + for (faustDisto& stage : impl._stages[c]) + stage.instanceClear(); + } + + for (unsigned c = 0; c < EffectChannels; ++c) { + impl._toneLpfMem[c] = 0.0f; + impl._up2x[c].clear_buffers(); + impl._up4x[c].clear_buffers(); + impl._up8x[c].clear_buffers(); + impl._down2x[c].clear_buffers(); + impl._down4x[c].clear_buffers(); + impl._down8x[c].clear_buffers(); + } +} + +void Disto::process(const float* const inputs[], float* const outputs[], unsigned nframes) +{ + // Note(jpc): assumes `inputs` and `outputs` to be different buffers + + Impl& impl = *_impl; + const float dry = impl._dry; + const float wet = impl._wet; + const float depth = impl._depth; + const float toneLpfPole = std::exp(float(-2.0 * M_PI) * impl.toneCutoff() * impl._samplePeriod); + + for (unsigned c = 0; c < EffectChannels; ++c) { + // compute LPF + absl::Span channelIn(inputs[c], nframes); + absl::Span lpfOut(outputs[c], nframes); + float lpfMem = impl._toneLpfMem[c]; + for (unsigned i = 0; i < nframes; ++i) { + // Note(jpc) apply `dry` gain, note there is no output if + // `dry=0 wet=`, it is the same behavior as reference + lpfMem = channelIn[i] * dry * (1.0f - toneLpfPole) + lpfMem * toneLpfPole; + lpfOut[i] = lpfMem; + } + impl._toneLpfMem[c] = lpfMem; + + // upsample to 8x + absl::Span temp[2] = { + absl::Span(impl._temp8x[0].get(), 8 * nframes), + absl::Span(impl._temp8x[1].get(), 8 * nframes), + }; + impl._up2x[c].process_block(temp[0].data(), lpfOut.data(), nframes); + impl._up4x[c].process_block(temp[1].data(), temp[0].data(), 2 * nframes); + impl._up8x[c].process_block(temp[0].data(), temp[1].data(), 4 * nframes); + absl::Span upsamplerOut = temp[0]; + + // run disto stages + absl::Span stageInOut = upsamplerOut; + for (unsigned s = 0, numStages = impl._numStages; s < numStages; ++s) { + // set depth parameter (TODO modulation) + impl.set_Depth(c, s, depth); + // + float *faustIn[] = { stageInOut.data() }; + float *faustOut[] = { stageInOut.data() }; + impl._stages[c][s].compute(8 * nframes, faustIn, faustOut); + } + + // downsample to 1x + impl._down8x[c].process_block(temp[1].data(), stageInOut.data(), 4 * nframes); + impl._down4x[c].process_block(temp[0].data(), temp[1].data(), 2 * nframes); + impl._down2x[c].process_block(outputs[c], temp[0].data(), nframes); + + // dry/wet mix + absl::Span mixOut(outputs[c], nframes); + for (unsigned i = 0; i < nframes; ++i) + mixOut[i] = mixOut[i] * wet + channelIn[i] * (1.0f - wet); + } +} + +std::unique_ptr Disto::makeInstance(absl::Span members) +{ + Disto* disto = new Disto; + std::unique_ptr fx { disto }; + + Impl& impl = *disto->_impl; + + for (const Opcode& opc : members) { + switch (opc.lettersOnlyHash) { + case hash("disto_tone"): + setValueFromOpcode(opc, impl._tone, {0.0f, 100.0f}); + break; + case hash("disto_depth"): + setValueFromOpcode(opc, impl._depth, {0.0f, 100.0f}); + break; + case hash("disto_stages"): + setValueFromOpcode(opc, impl._numStages, {1, Impl::maxStages}); + break; + case hash("disto_dry"): + if (auto value = readOpcode(opc.value, {0.0f, 100.0f})) + impl._dry = *value * 0.01f; + break; + case hash("disto_wet"): + if (auto value = readOpcode(opc.value, {0.0f, 100.0f})) + impl._wet = *value * 0.01f; + break; + } + } + + return fx; +} + +} // namespace sfz +} // namespace fx diff --git a/src/sfizz/effects/Disto.h b/src/sfizz/effects/Disto.h new file mode 100644 index 00000000..d9cc1ac7 --- /dev/null +++ b/src/sfizz/effects/Disto.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Effects.h" +#include + +namespace sfz { +namespace fx { + + /** + * @brief Distortion effect + */ + class Disto : public Effect { + public: + Disto(); + ~Disto(); + + /** + * @brief Initializes with the given sample rate. + */ + void setSampleRate(double sampleRate) override; + + /** + * @brief Sets the maximum number of frames to render at a time. The actual + * value can be lower but should never be higher. + */ + void setSamplesPerBlock(int samplesPerBlock) override; + + /** + * @brief Reset the state to initial. + */ + void clear() override; + + /** + * @brief Copy the input signal to the output + */ + void process(const float* const inputs[], float* const outputs[], unsigned nframes) override; + + /** + * @brief Instantiates given the contents of the block. + */ + static std::unique_ptr makeInstance(absl::Span members); + + private: + struct Impl; + std::unique_ptr _impl; + }; + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/dsp/disto_stage.dsp b/src/sfizz/effects/dsp/disto_stage.dsp new file mode 100644 index 00000000..f05a54fa --- /dev/null +++ b/src/sfizz/effects/dsp/disto_stage.dsp @@ -0,0 +1,40 @@ +import("stdfaust.lib"); + +disto_stage(depth, x) = shs*hh(x)+(1.0-shs)*lh(x) : fi.dcblockerat(5.0) with { + over = fconstant(int _oversampling, ); + + // sigmoid parameters + a = depth*0.2+2.0; + b = 2.0; + + // smooth hysteresis transition + shs = hs : si.smooth(ba.tau2pole(10e-3*over)); + + // the low and high hysteresis + lh(x) = sig(a*x)*b; + hh(x) = (sig(a*x)-1.0)*b; + + // + sig10 = environment { // sigmoid sampled from -10 to +10 + tablesize = 256; + table(i) = rdtable(tablesize, exact(float(ba.time)/float(tablesize)*20.0-10.0), i); + exact(x) = exp(x)/(exp(x)+1.0); + approx(x) = s1+mu*(s2-s1) with { + index = (x+10.0)*(1.0/20.0)*(sig10.tablesize-1) : max(0.0); + mu = index-int(index); + s1 = sig10.table(int(index) : min(sig10.tablesize-1)); + s2 = sig10.table(int(index) : +(1) : min(sig10.tablesize-1)); + }; + }; + + //sig = sig10.exact; + sig = sig10.approx; +} +letrec { + // hysteresis selection + 'hs = ba.if((xx') & (x>0.25), 0, hs)); +}; + +process = disto_stage(d) with { + d = hslider("[1] Depth", 100.0, 0.0, 100.0, 0.01); +}; diff --git a/src/sfizz/effects/gen/disto_stage.cxx b/src/sfizz/effects/gen/disto_stage.cxx new file mode 100644 index 00000000..7353b7ed --- /dev/null +++ b/src/sfizz/effects/gen/disto_stage.cxx @@ -0,0 +1,248 @@ +/* ------------------------------------------------------------ +name: "disto_stage" +Code generated with Faust 2.27.2 (https://faust.grame.fr) +Compilation options: -lang cpp -inpl -scal -ftz 0 +------------------------------------------------------------ */ + +#ifndef __faustDisto_H__ +#define __faustDisto_H__ + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +#include +#include +#include + +class faustDistoSIG0 { + + public: + + int iRec3[2]; + + public: + + int getNumInputsfaustDistoSIG0() { + return 0; + } + int getNumOutputsfaustDistoSIG0() { + return 1; + } + int getInputRatefaustDistoSIG0(int channel) { + int rate; + switch ((channel)) { + default: { + rate = -1; + break; + } + } + return rate; + } + int getOutputRatefaustDistoSIG0(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 0; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + + void instanceInitfaustDistoSIG0(int sample_rate) { + for (int l3 = 0; (l3 < 2); l3 = (l3 + 1)) { + iRec3[l3] = 0; + } + } + + void fillfaustDistoSIG0(int count, float* table) { + for (int i = 0; (i < count); i = (i + 1)) { + iRec3[0] = (iRec3[1] + 1); + float fTemp1 = std::exp(((0.078125f * float((iRec3[0] + -1))) + -10.0f)); + table[i] = (fTemp1 / (fTemp1 + 1.0f)); + iRec3[1] = iRec3[0]; + } + } + +}; + +static faustDistoSIG0* newfaustDistoSIG0() { return (faustDistoSIG0*)new faustDistoSIG0(); } +static void deletefaustDistoSIG0(faustDistoSIG0* dsp) { delete dsp; } + +static float ftbl0faustDistoSIG0[256]; + +#ifndef FAUSTCLASS +#define FAUSTCLASS faustDisto +#endif + +#ifdef __APPLE__ +#define exp10f __exp10f +#define exp10 __exp10 +#endif + +class faustDisto { + + public: + + float fVec0[2]; + int fSampleRate; + float fConst0; + float fConst1; + float fConst2; + float fConst3; + float fConst4; + int iConst5; + float fConst6; + int iRec2[2]; + float fConst7; + float fRec1[2]; + FAUSTFLOAT fHslider0; + float fVec1[2]; + float fRec0[2]; + + public: + + void metadata() { + } + + int getNumInputs() { + return 1; + } + int getNumOutputs() { + return 1; + } + int getInputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + int getOutputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + + static void classInit(int sample_rate) { + faustDistoSIG0* sig0 = newfaustDistoSIG0(); + sig0->instanceInitfaustDistoSIG0(sample_rate); + sig0->fillfaustDistoSIG0(256, ftbl0faustDistoSIG0); + deletefaustDistoSIG0(sig0); + } + + void instanceConstants(int sample_rate) { + fSampleRate = sample_rate; + fConst0 = std::min(192000.0f, std::max(1.0f, float(fSampleRate))); + fConst1 = (15.707963f / fConst0); + fConst2 = (1.0f / (fConst1 + 1.0f)); + fConst3 = (1.0f - fConst1); + fConst4 = (0.00999999978f * float(_oversampling)); + iConst5 = (std::fabs(fConst4) < 1.1920929e-07f); + fConst6 = (iConst5 ? 0.0f : std::exp((0.0f - ((1.0f / fConst0) / (iConst5 ? 1.0f : fConst4))))); + fConst7 = (1.0f - fConst6); + } + + void instanceResetUserInterface() { + fHslider0 = FAUSTFLOAT(100.0f); + } + + void instanceClear() { + for (int l0 = 0; (l0 < 2); l0 = (l0 + 1)) { + fVec0[l0] = 0.0f; + } + for (int l1 = 0; (l1 < 2); l1 = (l1 + 1)) { + iRec2[l1] = 0; + } + for (int l2 = 0; (l2 < 2); l2 = (l2 + 1)) { + fRec1[l2] = 0.0f; + } + for (int l4 = 0; (l4 < 2); l4 = (l4 + 1)) { + fVec1[l4] = 0.0f; + } + for (int l5 = 0; (l5 < 2); l5 = (l5 + 1)) { + fRec0[l5] = 0.0f; + } + } + + void init(int sample_rate) { + classInit(sample_rate); + instanceInit(sample_rate); + } + void instanceInit(int sample_rate) { + instanceConstants(sample_rate); + instanceResetUserInterface(); + instanceClear(); + } + + faustDisto* clone() { + return new faustDisto(); + } + + int getSampleRate() { + return fSampleRate; + } + + void buildUserInterface() { + } + + void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { + FAUSTFLOAT* input0 = inputs[0]; + FAUSTFLOAT* output0 = outputs[0]; + float fSlow0 = ((0.200000003f * float(fHslider0)) + 2.0f); + for (int i = 0; (i < count); i = (i + 1)) { + float fTemp0 = float(input0[i]); + fVec0[0] = fTemp0; + iRec2[0] = (((fTemp0 < fVec0[1]) & (fTemp0 < -0.25f)) ? 1 : (((fTemp0 > fVec0[1]) & (fTemp0 > 0.25f)) ? 0 : iRec2[1])); + fRec1[0] = ((fRec1[1] * fConst6) + (float(iRec2[0]) * fConst7)); + float fTemp2 = std::max(0.0f, (12.75f * ((fSlow0 * fTemp0) + 10.0f))); + int iTemp3 = int(fTemp2); + float fTemp4 = ftbl0faustDistoSIG0[std::min(255, iTemp3)]; + float fTemp5 = (fTemp4 + ((fTemp2 - float(iTemp3)) * (ftbl0faustDistoSIG0[std::min(255, (iTemp3 + 1))] - fTemp4))); + float fTemp6 = ((fRec1[0] * (fTemp5 + -1.0f)) + ((1.0f - fRec1[0]) * fTemp5)); + fVec1[0] = fTemp6; + fRec0[0] = (fConst2 * ((fConst3 * fRec0[1]) + (2.0f * (fTemp6 - fVec1[1])))); + output0[i] = FAUSTFLOAT(fRec0[0]); + fVec0[1] = fVec0[0]; + iRec2[1] = iRec2[0]; + fRec1[1] = fRec1[0]; + fVec1[1] = fVec1[0]; + fRec0[1] = fRec0[0]; + } + } + +}; + +#ifdef FAUST_UIMACROS + + + + #define FAUST_LIST_ACTIVES(p) \ + p(HORIZONTALSLIDER, Depth, "Depth", fHslider0, 100.0f, 0.0f, 100.0f, 0.01f) \ + + #define FAUST_LIST_PASSIVES(p) \ + +#endif + +#endif From 823088b629742147f7faff81ac8b164e24c912fb Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 13:52:49 +0200 Subject: [PATCH 025/445] Fix the LPF cutoff --- src/sfizz/effects/Disto.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sfizz/effects/Disto.cpp b/src/sfizz/effects/Disto.cpp index 4aea5188..9e83856a 100644 --- a/src/sfizz/effects/Disto.cpp +++ b/src/sfizz/effects/Disto.cpp @@ -21,6 +21,7 @@ #include "Disto.h" #include "Opcode.h" #include "Config.h" +#include "MathHelpers.h" #include #include #include @@ -57,7 +58,11 @@ struct Disto::Impl { std::unique_ptr _temp8x[2]; // use the same formula as reverb - float toneCutoff() const noexcept { return 21.0f + _tone * 1.08f; } + float toneCutoff() const noexcept + { + float mk = 21.0f + _tone * 1.08f; + return 440.0f * std::exp2((mk - 69.0f) * (1.0f / 12.0f)); + } #define DEFINE_SET_GET(type, ident, name, var, def, min, max, step) \ float get_##ident(size_t c, size_t s) const noexcept { return _stages[c][s].var; } \ From 0c906577dc303e52d40da36788b9a0ce3dcc9312 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 29 Jul 2020 20:38:55 +0200 Subject: [PATCH 026/445] Add the reverb --- dpf.mk | 1 + scripts/generate_fverb.sh | 54 +++ src/CMakeLists.txt | 5 +- src/sfizz/Effects.cpp | 2 + src/sfizz/effects/Fverb.cpp | 247 +++++++++++ src/sfizz/effects/Fverb.h | 54 +++ src/sfizz/effects/dsp/fverb.dsp | 183 ++++++++ src/sfizz/effects/gen/fverb.cxx | 713 ++++++++++++++++++++++++++++++++ 8 files changed, 1258 insertions(+), 1 deletion(-) create mode 100755 scripts/generate_fverb.sh create mode 100644 src/sfizz/effects/Fverb.cpp create mode 100644 src/sfizz/effects/Fverb.h create mode 100644 src/sfizz/effects/dsp/fverb.dsp create mode 100644 src/sfizz/effects/gen/fverb.cxx diff --git a/dpf.mk b/dpf.mk index f79e7d41..3cef759a 100644 --- a/dpf.mk +++ b/dpf.mk @@ -64,6 +64,7 @@ SFIZZ_SOURCES = \ src/sfizz/effects/Disto.cpp \ src/sfizz/effects/Eq.cpp \ src/sfizz/effects/Filter.cpp \ + src/sfizz/effects/Fverb.cpp \ src/sfizz/effects/Gain.cpp \ src/sfizz/effects/Gate.cpp \ src/sfizz/effects/impl/ResonantArrayAVX.cpp \ diff --git a/scripts/generate_fverb.sh b/scripts/generate_fverb.sh new file mode 100755 index 00000000..7c997e2b --- /dev/null +++ b/scripts/generate_fverb.sh @@ -0,0 +1,54 @@ +#!/bin/sh +set -e + +if ! test -d "src"; then + echo "Please run this in the project root directory." + exit 1 +fi + +# Note: needs faust >= 2.27.1 for UI macros +FAUSTARGS="-uim -inpl" + +# support GNU sed only, use gsed on a Mac +test -z "$SED" && SED=sed + +faustgen() { + mkdir -p src/sfizz/effects/gen + local outfile=src/sfizz/effects/gen/fverb.cxx + + local code=`faust $FAUSTARGS -cn faustFverb src/sfizz/effects/dsp/fverb.dsp` + + # suppress some faust-specific stuff we don't care + echo "$code" \ + | fgrep -v -- '->declare(' \ + | fgrep -v -- '->openHorizontalBox(' \ + | fgrep -v -- '->openVerticalBox(' \ + | fgrep -v -- '->closeBox(' \ + | fgrep -v -- '->addHorizontalSlider(' \ + | fgrep -v -- '->addVerticalSlider(' \ + > "$outfile" + + # remove metadata + $SED -r -i 's/void[ \t]+metadata[ \t]*\(Meta[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void metadata()/' "$outfile" + + # remove UI + $SED -r -i 's/void[ \t]+buildUserInterface[ \t]*\(UI[ \t]*\*[ \t]*[a-zA-Z0-9_]+\)/void buildUserInterface()/' "$outfile" + + # remove inheritance + $SED -r -i 's/:[ \t]*public[ \t]+dsp\b\s*//' "$outfile" + + # remove virtual + $SED -r -i 's/\bvirtual\b\s*//' "$outfile" + + # remove undesired UIM + $SED -r -i '/^[ \t]*#define[ \t]+FAUST_(FILE_NAME|CLASS_NAME|INPUTS|OUTPUTS|ACTIVES|PASSIVES)/d' "$outfile" + $SED -r -i '/^[ \t]*FAUST_ADD.*/d' "$outfile" + + # direct access to parameter variables + $SED -r -i 's/\bprivate:/public:/' "$outfile" + + # remove trailing whitespace + $SED -r -i 's/[ \t]+$//' "$outfile" +} + +faustgen diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6d0d5c83..7f1c9dcc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,7 +12,8 @@ set (FAUST_FILES sfizz/effects/dsp/resonant_string.dsp sfizz/effects/dsp/compressor.dsp sfizz/effects/dsp/gate.dsp - sfizz/effects/dsp/disto_stage.dsp) + sfizz/effects/dsp/disto_stage.dsp + sfizz/effects/dsp/fverb.dsp) source_group ("Faust Files" FILES ${FAUST_FILES}) set (SFIZZ_HEADERS @@ -39,6 +40,7 @@ set (SFIZZ_HEADERS sfizz/effects/Disto.h sfizz/effects/Eq.h sfizz/effects/Filter.h + sfizz/effects/Fverb.h sfizz/effects/Gain.h sfizz/effects/Gate.h sfizz/effects/Limiter.h @@ -134,6 +136,7 @@ set (SFIZZ_SOURCES sfizz/effects/Gate.cpp sfizz/effects/Disto.cpp sfizz/effects/Strings.cpp + sfizz/effects/Fverb.cpp sfizz/effects/Rectify.cpp sfizz/effects/Gain.cpp sfizz/effects/Width.cpp diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index cc9254b1..da99d98c 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -19,6 +19,7 @@ #include "effects/Gate.h" #include "effects/Disto.h" #include "effects/Strings.h" +#include "effects/Fverb.h" #include "effects/Rectify.h" #include "effects/Gain.h" #include "effects/Width.h" @@ -38,6 +39,7 @@ void EffectFactory::registerStandardEffectTypes() registerEffectType("gate", fx::Gate::makeInstance); registerEffectType("disto", fx::Disto::makeInstance); registerEffectType("strings", fx::Strings::makeInstance); + registerEffectType("fverb", fx::Fverb::makeInstance); // extensions (book) registerEffectType("rectify", fx::Rectify::makeInstance); diff --git a/src/sfizz/effects/Fverb.cpp b/src/sfizz/effects/Fverb.cpp new file mode 100644 index 00000000..9dbf04d6 --- /dev/null +++ b/src/sfizz/effects/Fverb.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Fverb.h" +#include "Opcode.h" +#include "Config.h" +#include "MathHelpers.h" +#include +#include +#include +#define FAUST_UIMACROS 1 +#include "gen/fverb.cxx" + +/** + Note(jpc): implementation status + +- [x] reverb_type +- [x] reverb_dry +- [ ] reverb_dry_oncc +- [x] reverb_wet +- [ ] reverb_wet_oncc +- [x] reverb_input +- [ ] reverb_input_oncc +- [x] reverb_size +- [ ] reverb_size_oncc +- [x] reverb_predelay +- [ ] reverb_predelay_oncc +- [x] reverb_tone +- [ ] reverb_tone_oncc +- [x] reverb_damp +- [ ] reverb_damp_oncc + */ + +namespace sfz { +namespace fx { + + struct Fverb::Impl { + faustFverb dsp; + + #define DEFINE_SET_GET(type, ident, name, var, def, min, max, step) \ + float get_##ident() const noexcept { return dsp.var; } \ + void set_##ident(float value) noexcept { dsp.var = value; } + FAUST_LIST_ACTIVES(DEFINE_SET_GET); + #undef DEFINE_SET_GET + + struct Profile { + float tailDensity; // % + float decayAtMaxSize; // % + float modulationFrequency; // Hz + float modulationDepth; // ms + float dry; // % + float wet; // % + }; + + static const Profile largeRoom; + static const Profile midRoom; + static const Profile smallRoom; + static const Profile largeHall; + static const Profile midHall; + static const Profile smallHall; + + static double lpfCutoff(double x) + { + double midiPitch = 21.0 + clamp(x, 0.0, 100.0) * 1.08; + return 440.0 * std::exp2((midiPitch - 69.0) * (1.0 / 12.0)); + } + }; + + /// + const Fverb::Impl::Profile Fverb::Impl::largeRoom { + 80, // tail density + 65, // decay at max size + 0.6, // modulation frequency + 0.5, // modulation depth + 100, // dry + 60, // wet + }; + const Fverb::Impl::Profile Fverb::Impl::midRoom { + 50, // tail density + 50, // decay at max size + 1.25, // modulation frequency + 0.5, // modulation depth + 100, // dry + 60, // wet + }; + const Fverb::Impl::Profile Fverb::Impl::smallRoom { + 20, // tail density + 5, // decay at max size + 1.5, // modulation frequency + 0.5, // modulation depth + 100, // dry + 60, // wet + }; + const Fverb::Impl::Profile Fverb::Impl::largeHall { + 80, // tail density + 90, // decay at max size + 0.275, // modulation frequency + 1.5, // modulation depth + 100, // dry + 60, // wet + }; + const Fverb::Impl::Profile Fverb::Impl::midHall { + 50, // tail density + 75, // decay at max size + 0.5, // modulation frequency + 1.5, // modulation depth + 100, // dry + 60, // wet + }; + const Fverb::Impl::Profile Fverb::Impl::smallHall { + 20, // tail density + 50, // decay at max size + 0.65, // modulation frequency + 1.5, // modulation depth + 100, // dry + 60, // wet + }; + + /// + Fverb::Fverb() + : impl_(new Impl) + { + Impl& impl = *impl_; + auto& dsp = impl.dsp; + + dsp.init(config::defaultSampleRate); + } + + Fverb::~Fverb() + { + } + + void Fverb::setSampleRate(double sampleRate) + { + Impl& impl = *impl_; + auto& dsp = impl.dsp; + + dsp.classInit(sampleRate); + dsp.instanceConstants(sampleRate); + + clear(); + } + + void Fverb::setSamplesPerBlock(int samplesPerBlock) + { + (void)samplesPerBlock; + } + + void Fverb::clear() + { + Impl& impl = *impl_; + auto& dsp = impl.dsp; + + dsp.instanceClear(); + } + + void Fverb::process(const float* const inputs[], float* const outputs[], unsigned nframes) + { + Impl& impl = *impl_; + auto& dsp = impl.dsp; + + dsp.compute(nframes, const_cast(inputs), const_cast(outputs)); + } + + std::unique_ptr Fverb::makeInstance(absl::Span members) + { + Fverb* reverb = new Fverb; + std::unique_ptr fx { reverb }; + + const Impl::Profile* profile = &Impl::largeHall; + float dry = 0; + float wet = 0; + float input = 0; + float size = 0; + float predelay = 0; + float tone = 100; + float damp = 0; + + for (const Opcode& opc : members) { + switch (opc.lettersOnlyHash) { + case hash("reverb_type"): + { + std::string value = opc.value; + absl::AsciiStrToLower(&value); + if (value == "large_room") + profile = &Impl::largeRoom; + else if (value == "mid_room") + profile = &Impl::midRoom; + else if (value == "small_room") + profile = &Impl::smallRoom; + else if (value == "large_hall") + profile = &Impl::largeHall; + else if (value == "mid_hall") + profile = &Impl::midHall; + else if (value == "small_hall") + profile = &Impl::smallHall; + } + break; + case hash("reverb_dry"): + setValueFromOpcode(opc, dry, {0.0f, 100.0f}); + break; + case hash("reverb_wet"): + setValueFromOpcode(opc, wet, {0.0f, 100.0f}); + break; + case hash("reverb_input"): + setValueFromOpcode(opc, input, {0.0f, 100.0f}); + break; + case hash("reverb_size"): + setValueFromOpcode(opc, size, {0.0f, 100.0f}); + break; + case hash("reverb_predelay"): + setValueFromOpcode(opc, predelay, {0.0f, 1.0f}); + break; + case hash("reverb_tone"): + setValueFromOpcode(opc, tone, {0.0f, 100.0f}); + break; + case hash("reverb_damp"): + setValueFromOpcode(opc, damp, {0.0f, 100.0f}); + break; + } + } + + // NOTE(jpc) determine a range for decays 0-100. not calibrated + const float decayMax = profile->decayAtMaxSize; + const float decayMin = decayMax * 0.5f; + + Impl& impl = *reverb->impl_; + impl.set_Predelay(predelay * 1e3); + impl.set_Tail_density(profile->tailDensity); + impl.set_Decay(decayMax * size * 0.01f + decayMin * (1.0f - size * 0.01f)); + impl.set_Modulator_frequency(profile->modulationFrequency); + impl.set_Modulator_depth(profile->modulationDepth); + impl.set_Dry(profile->dry * dry * 0.01f); + impl.set_Wet(profile->wet * wet * 0.01f); + impl.set_Input_amount(input); + impl.set_Input_low_pass_cutoff(Impl::lpfCutoff(tone)); + // NOTE(jpc): damp formula not well calibrated, but sounds ok-ish + impl.set_Damping(Impl::lpfCutoff(100 - 0.5 * damp)); + + return fx; + } + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/Fverb.h b/src/sfizz/effects/Fverb.h new file mode 100644 index 00000000..718f269e --- /dev/null +++ b/src/sfizz/effects/Fverb.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Effects.h" +#include + +namespace sfz { +namespace fx { + + /** + * @brief Reverb effect + */ + class Fverb : public Effect { + public: + Fverb(); + ~Fverb(); + + /** + * @brief Initializes with the given sample rate. + */ + void setSampleRate(double sampleRate) override; + + /** + * @brief Sets the maximum number of frames to render at a time. The actual + * value can be lower but should never be higher. + */ + void setSamplesPerBlock(int samplesPerBlock) override; + + /** + * @brief Reset the state to initial. + */ + void clear() override; + + /** + * @brief Copy the input signal to the output + */ + void process(const float* const inputs[], float* const outputs[], unsigned nframes) override; + + /** + * @brief Instantiates given the contents of the block. + */ + static std::unique_ptr makeInstance(absl::Span members); + + private: + struct Impl; + std::unique_ptr impl_; + }; + +} // namespace fx +} // namespace sfz diff --git a/src/sfizz/effects/dsp/fverb.dsp b/src/sfizz/effects/dsp/fverb.dsp new file mode 100644 index 00000000..30996e64 --- /dev/null +++ b/src/sfizz/effects/dsp/fverb.dsp @@ -0,0 +1,183 @@ +// +// Référence: +// Dattorro, Jon. "Effect design, part 1: Reverberator and other filters." +// Journal of the Audio Engineering Society 45.9 (1997): 660-684. +// + +// Note(jpc): faust 2.27.1 lets us take advantage of -uim; +// however, avoid use special chars in control names (eg. '-'). + +declare name "fverb"; +declare author "Jean Pierre Cimalando"; +declare version "0.5"; +declare license "BSD-2-Clause"; + +import("stdfaust.lib"); + +ptMax = 300e-3; +pt = hslider("[01] Predelay [symbol:predelay] [unit:ms]", 0., 0., ptMax*1e3, 1.) : *(1e-3) : si.smoo; +ing = hslider("[02] Input amount [symbol:input] [unit:%]", 100., 0., 100., 0.01) : *(0.01) : si.smoo; +tone = hslider("[03] Input low pass cutoff [symbol:input_lowpass] [unit:Hz] [scale:log]", 10000., 1., 20000., 1.); +htone = hslider("[04] Input high pass cutoff [symbol:input_highpass] [unit:Hz] [scale:log]", 100., 1., 1000., 1.); +id1 = hslider("[05] Input diffusion 1 [symbol:input_diffusion_1] [unit:%]", 75., 0., 100., 0.01) : *(0.01) : si.smoo; +id2 = hslider("[06] Input diffusion 2 [symbol:input_diffusion_2] [unit:%]", 62.5, 0., 100., 0.01) : *(0.01) : si.smoo; +dd1 = hslider("[07] Tail density [symbol:tail_density] [unit:%]", 70., 0., 100., 0.01) : *(0.01) : si.smoo; +dd2 = (dr + 0.15) : max(0.25) : min(0.5); /* (cf. table 1 Reverberation parameters) */ +dr = hslider("[08] Decay [symbol:decay] [unit:%]", 50., 0., 100., 0.01) : *(0.01) : si.smoo; +damp = hslider("[09] Damping [symbol:damping] [unit:Hz] [scale:log]", 5500., 10., 20000., 1.); +modf = /*1.0*/hslider("[10] Modulator frequency [symbol:mod_frequency] [unit:Hz]", 1., 0.01, 4., 0.01) : si.smoo; +maxModt = 10e-3; +modt = hslider("[11] Modulator depth [symbol:mod_depth] [unit:ms]", 0.5, 0., maxModt*1e3, 0.1) : *(1e-3) : si.smoo; +dry = hslider("[12] Dry [symbol:dry] [unit:%]", 100., 0., 100., 0.01) : *(0.01) : si.smoo; +wet = hslider("[13] Wet [symbol:wet] [unit:%]", 50., 0., 100., 0.01) : *(0.01) : si.smoo; +/* 0:full stereo, 1:full mono */ +cmix = 0.; //hslider("[12] Stereo cross mix", 0., 0., 1., 0.01) : *(0.5); + +/* for complete control of decay parameters */ +// dd1 = hslider("[05] Decay diffusion 1 [unit:%]", 70., 0., 100., 0.01) : *(0.01) : si.smoo; +// dd2 = hslider("[06] Decay diffusion 2 [unit:%]", 50., 0., 100., 0.01) : *(0.01) : si.smoo; + +fverb(lIn, rIn) = + ((preInL : preInjectorL), (preInR : preInjectorR)) : + crossInjector(ff1A, ff1B, ff1C, fb1, ff2A, ff2B, ff2C, fb2) : + outputReconstruction +with { + // this reverb was designed for nominal rate of 29761 Hz + T(x) = x/refSR with { refSR = 29761.; }; // reference time to seconds + + // stereo input (reference was mono downmixed) + preInL = (1.-cmix)*lIn+cmix*rIn : *(ing); + preInR = (1.-cmix)*rIn+cmix*lIn : *(ing); + + /* before entry into tank */ + /* Note(jpc) different delays left and right in hope to decorrelate more. + values not documented anywhere, just out of my magic hat */ + preInjectorL = predelay : toneLpf(tone) : toneHpf(htone) : + diffusion(id1, 1.03*T(142)) : diffusion(id1, 0.97*T(107)) : + diffusion(id2, 0.97*T(379)) : diffusion(id2, 1.03*T(277)); + preInjectorR = predelay : toneLpf(tone) : toneHpf(htone) : + diffusion(id1, 0.97*T(142)) : diffusion(id1, 1.03*T(107)) : + diffusion(id2, 1.03*T(379)) : diffusion(id2, 0.97*T(277)); + /* the default for mixed down mono input */ + // preInjector = predelay : toneLpf(tone) : + // diffusion(id1, T(142)) : diffusion(id1, T(107)) : + // diffusion(id2, T(379)) : diffusion(id2, T(277)); + + /* + (cf. 1.3.7 Delay Modulation) + Linear delay interpolation introduces undesired damping artifacts, + this problem is resolved by using all-pass interpolation instead. + + Note(jpc) I'm told Dual Delay Interpolation aka `sdelay` works better and + exhibits less artifacts. The choice of time constant is for now + arbitrary, based on some hints in the documentation of `sdelay`. + */ + fcomb = ddi(10e-3)/*allpass*/ with { + linear = fi.allpass_fcomb; + lagrange = fi.allpass_fcomb5; + allpass = fi.allpass_fcomb1a; + ddi(it, maxdel, N, aN) = (+ <: de.sdelay(maxdel, int(ma.SR*it), N-1),*(aN)) ~ *(-aN) : mem,_ : +; + }; + + delayDim(t) = 65536; // TODO(jpc) expression below does not work? + //delayDim(t) = ma.nextpow2(t*maxSR) with { maxSR = 192000. }; + + predelay = de.delay(delayDim(ptMax), int(pt*ma.SR)); + toneLpf(f) = fi.iir((1.-p), (0.-p)) with { p = exp(-2.*ma.PI*f/ma.SR) : si.smoo; }; + toneHpf(f) = fi.iir((0.5*(1.+p),-0.5*(1.+p)), (0.-p)) with { p = exp(-2.*ma.PI*f/ma.SR) : si.smoo; }; + + /* note(jpc) round fixed delays to samples to make it faster */ + diffusion(amt, del) = fi.allpass_comb/*fcomb*/(delayDim(del), int(del*ma.SR), amt); + + dd1Mod1 = dd1OscPair : (_, !); + //dd1Mod2 = dd1Mod1; + /* + (cf. 1.3.7 Delay Modulation) + A different secondary oscillator can decorrelate the signal further and + create more resonances. + */ + dd1Mod2 = dd1OscPair : (!, _); + + /* prefer a quadrature oscillator if frequency is fixed */ + //dd1OscPair = os.oscq(modf); + /* otherwise use a phase-synchronized pair */ + dd1OscPair = sine(p), cosine(p) with { + sine(p) = rdtable(tablesize, os.sinwaveform(tablesize), int(p*tablesize)); + cosine(p) = sine(wrap(p+0.25)); + tablesize = 1 << 16; + } + letrec { + 'p = wrap(p+modf*(1./ma.SR)); + }; + wrap(p) = p-int(p); + + fixedDelay(t) = de.delay(delayDim(t), int(ma.SR*t)); + modulatedFcomb(t, tMaxExc, tMod, g) = fcomb(delayDim(t+tMaxExc), int(ma.SR*(t+tMod)), g); + + ff1A = modulatedFcomb(T(762), maxModt, dd1Mod1*modt, ma.neg(dd1)); + ff1B = fixedDelay(T(4453)) : toneLpf(damp); + ff1C = *(dr) : diffusion(ma.neg(dd2), T(1800)); + fb1 = fixedDelay(T(3720)) : *(dr); + ff2A = modulatedFcomb(T(908), maxModt, dd1Mod2*modt, ma.neg(dd1)); + ff2B = fixedDelay(T(4217)) : toneLpf(damp); + ff2C = *(dr) : diffusion(ma.neg(dd2), T(2656)); + fb2 = fixedDelay(T(3163)) : *(dr); + + outputReconstruction(n1, n2, n3, n4, n5, n6) = + 0.6*sum(i, 7, lTap(i)), 0.6*sum(i, 7, rTap(i)) + with { + lTap(0) = n4 : fixedDelay(T(266)); + lTap(1) = n4 : fixedDelay(T(2974)); + lTap(2) = n5 : fixedDelay(T(1913)) : ma.neg; + lTap(3) = n6 : fixedDelay(T(1996)); + lTap(4) = n1 : fixedDelay(T(1990)) : ma.neg; + lTap(5) = n2 : fixedDelay(T(187)) : ma.neg; + lTap(6) = n3 : fixedDelay(T(1066)) : ma.neg; + // + rTap(0) = n1 : fixedDelay(T(353)); + rTap(1) = n1 : fixedDelay(T(3627)); + rTap(2) = n2 : fixedDelay(T(1228)) : ma.neg; + rTap(3) = n3 : fixedDelay(T(2673)); + rTap(4) = n4 : fixedDelay(T(2111)) : ma.neg; + rTap(5) = n5 : fixedDelay(T(335)) : ma.neg; + rTap(6) = n6 : fixedDelay(T(121)) : ma.neg; + }; + + /* + * A1 B1 C1 + * ^ ^ ^ + * | | | + * in1 -> [+] ----> [ . ff1 . ] >--.---. + * ^ | + * | | + * .----< [fb1] <--- [z-1] <-------. + * | | + * .----< [fb2] <--- [z-1] <---. | + * | | + * v | + * in2 -> [+] ----> [ . ff2 . ] >--.-------. + * | | | + * v v v + * A2 B2 C2 + * + * note: implicit unit delay in the feedback paths + */ + crossInjector( + ff1A, ff1B, ff1C, fb1, + ff2A, ff2B, ff2C, fb2, + in1, in2) = + A1, B1, C1, + A2, B2, C2 + letrec { + 'A1 = C2 : fb1 : +(in1) : ff1A; + 'B1 = C2 : fb1 : +(in1) : ff1A : ff1B; + 'C1 = C2 : fb1 : +(in1) : ff1A : ff1B : ff1C; + 'A2 = C1 : fb2 : +(in2) : ff2A; + 'B2 = C1 : fb2 : +(in2) : ff2A : ff2B; + 'C2 = C1 : fb2 : +(in2) : ff2A : ff2B : ff2C; + }; +}; + +process(l, r) = fverb(l, r) : mix with { + mix(rl, rr) = dry*l+wet*rl, dry*r+wet*rr; +}; diff --git a/src/sfizz/effects/gen/fverb.cxx b/src/sfizz/effects/gen/fverb.cxx new file mode 100644 index 00000000..07a742d8 --- /dev/null +++ b/src/sfizz/effects/gen/fverb.cxx @@ -0,0 +1,713 @@ +/* ------------------------------------------------------------ +author: "Jean Pierre Cimalando" +license: "BSD-2-Clause" +name: "fverb" +version: "0.5" +Code generated with Faust 2.27.1 (https://faust.grame.fr) +Compilation options: -lang cpp -inpl -scal -ftz 0 +------------------------------------------------------------ */ + +#ifndef __faustFverb_H__ +#define __faustFverb_H__ + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +#include +#include +#include + +class faustFverbSIG0 { + + public: + + int iRec19[2]; + + public: + + int getNumInputsfaustFverbSIG0() { + return 0; + } + int getNumOutputsfaustFverbSIG0() { + return 1; + } + int getInputRatefaustFverbSIG0(int channel) { + int rate; + switch ((channel)) { + default: { + rate = -1; + break; + } + } + return rate; + } + int getOutputRatefaustFverbSIG0(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 0; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + + void instanceInitfaustFverbSIG0(int sample_rate) { + for (int l4 = 0; (l4 < 2); l4 = (l4 + 1)) { + iRec19[l4] = 0; + } + } + + void fillfaustFverbSIG0(int count, float* table) { + for (int i = 0; (i < count); i = (i + 1)) { + iRec19[0] = (iRec19[1] + 1); + table[i] = std::sin((9.58738019e-05f * float((iRec19[0] + -1)))); + iRec19[1] = iRec19[0]; + } + } + +}; + +static faustFverbSIG0* newfaustFverbSIG0() { return (faustFverbSIG0*)new faustFverbSIG0(); } +static void deletefaustFverbSIG0(faustFverbSIG0* dsp) { delete dsp; } + +static float ftbl0faustFverbSIG0[65536]; + +#ifndef FAUSTCLASS +#define FAUSTCLASS faustFverb +#endif + +#ifdef __APPLE__ +#define exp10f __exp10f +#define exp10 __exp10 +#endif + +class faustFverb { + + public: + + FAUSTFLOAT fHslider0; + float fRec0[2]; + FAUSTFLOAT fHslider1; + float fRec1[2]; + FAUSTFLOAT fHslider2; + float fRec10[2]; + int fSampleRate; + float fConst0; + FAUSTFLOAT fHslider3; + float fRec18[2]; + float fConst1; + FAUSTFLOAT fHslider4; + float fRec21[2]; + float fRec20[2]; + float fConst2; + float fConst3; + float fRec14[2]; + float fRec15[2]; + int iRec16[2]; + int iRec17[2]; + FAUSTFLOAT fHslider5; + float fRec32[2]; + int IOTA; + float fVec0[131072]; + FAUSTFLOAT fHslider6; + float fRec33[2]; + FAUSTFLOAT fHslider7; + float fRec34[2]; + float fRec31[2]; + FAUSTFLOAT fHslider8; + float fRec35[2]; + float fRec30[2]; + FAUSTFLOAT fHslider9; + float fRec36[2]; + float fVec1[1024]; + int iConst4; + float fRec28[2]; + float fVec2[1024]; + int iConst5; + float fRec26[2]; + FAUSTFLOAT fHslider10; + float fRec37[2]; + float fVec3[4096]; + int iConst6; + float fRec24[2]; + float fVec4[2048]; + int iConst7; + float fRec22[2]; + int iConst8; + FAUSTFLOAT fHslider11; + float fRec38[2]; + float fVec5[131072]; + float fRec12[2]; + float fVec6[32768]; + int iConst9; + FAUSTFLOAT fHslider12; + float fRec39[2]; + float fRec11[2]; + float fVec7[32768]; + int iConst10; + float fRec8[2]; + float fRec2[32768]; + float fRec3[16384]; + float fRec4[32768]; + float fRec45[2]; + float fRec46[2]; + int iRec47[2]; + int iRec48[2]; + float fVec8[131072]; + float fRec58[2]; + float fRec57[2]; + float fVec9[1024]; + int iConst11; + float fRec55[2]; + float fVec10[1024]; + int iConst12; + float fRec53[2]; + float fVec11[4096]; + int iConst13; + float fRec51[2]; + float fVec12[2048]; + int iConst14; + float fRec49[2]; + int iConst15; + float fVec13[131072]; + float fRec43[2]; + float fVec14[32768]; + int iConst16; + float fRec42[2]; + float fVec15[16384]; + int iConst17; + float fRec40[2]; + float fRec5[32768]; + float fRec6[8192]; + float fRec7[32768]; + int iConst18; + int iConst19; + int iConst20; + int iConst21; + int iConst22; + int iConst23; + int iConst24; + int iConst25; + int iConst26; + int iConst27; + int iConst28; + int iConst29; + int iConst30; + int iConst31; + + public: + + void metadata() { + } + + int getNumInputs() { + return 2; + } + int getNumOutputs() { + return 2; + } + int getInputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + case 1: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + int getOutputRate(int channel) { + int rate; + switch ((channel)) { + case 0: { + rate = 1; + break; + } + case 1: { + rate = 1; + break; + } + default: { + rate = -1; + break; + } + } + return rate; + } + + static void classInit(int sample_rate) { + faustFverbSIG0* sig0 = newfaustFverbSIG0(); + sig0->instanceInitfaustFverbSIG0(sample_rate); + sig0->fillfaustFverbSIG0(65536, ftbl0faustFverbSIG0); + deletefaustFverbSIG0(sig0); + } + + void instanceConstants(int sample_rate) { + fSampleRate = sample_rate; + fConst0 = std::min(192000.0f, std::max(1.0f, float(fSampleRate))); + fConst1 = (1.0f / fConst0); + fConst2 = (1.0f / float(int((0.00999999978f * fConst0)))); + fConst3 = (0.0f - fConst2); + iConst4 = std::min(65536, std::max(0, (int((0.00462820474f * fConst0)) + -1))); + iConst5 = std::min(65536, std::max(0, (int((0.00370316859f * fConst0)) + -1))); + iConst6 = std::min(65536, std::max(0, (int((0.013116831f * fConst0)) + -1))); + iConst7 = std::min(65536, std::max(0, (int((0.00902825873f * fConst0)) + -1))); + iConst8 = (std::min(65536, std::max(0, int((0.106280029f * fConst0)))) + 1); + iConst9 = std::min(65536, std::max(0, int((0.141695514f * fConst0)))); + iConst10 = std::min(65536, std::max(0, (int((0.0892443135f * fConst0)) + -1))); + iConst11 = std::min(65536, std::max(0, (int((0.00491448538f * fConst0)) + -1))); + iConst12 = std::min(65536, std::max(0, (int((0.00348745007f * fConst0)) + -1))); + iConst13 = std::min(65536, std::max(0, (int((0.0123527432f * fConst0)) + -1))); + iConst14 = std::min(65536, std::max(0, (int((0.00958670769f * fConst0)) + -1))); + iConst15 = (std::min(65536, std::max(0, int((0.124995798f * fConst0)))) + 1); + iConst16 = std::min(65536, std::max(0, int((0.149625346f * fConst0)))); + iConst17 = std::min(65536, std::max(0, (int((0.0604818389f * fConst0)) + -1))); + iConst18 = std::min(65536, std::max(0, int((0.00893787201f * fConst0)))); + iConst19 = std::min(65536, std::max(0, int((0.099929437f * fConst0)))); + iConst20 = std::min(65536, std::max(0, int((0.067067638f * fConst0)))); + iConst21 = std::min(65536, std::max(0, int((0.0642787516f * fConst0)))); + iConst22 = std::min(65536, std::max(0, int((0.0668660328f * fConst0)))); + iConst23 = std::min(65536, std::max(0, int((0.0062833908f * fConst0)))); + iConst24 = std::min(65536, std::max(0, int((0.0358186886f * fConst0)))); + iConst25 = std::min(65536, std::max(0, int((0.0118611604f * fConst0)))); + iConst26 = std::min(65536, std::max(0, int((0.121870905f * fConst0)))); + iConst27 = std::min(65536, std::max(0, int((0.0898155272f * fConst0)))); + iConst28 = std::min(65536, std::max(0, int((0.041262053f * fConst0)))); + iConst29 = std::min(65536, std::max(0, int((0.070931755f * fConst0)))); + iConst30 = std::min(65536, std::max(0, int((0.0112563418f * fConst0)))); + iConst31 = std::min(65536, std::max(0, int((0.00406572362f * fConst0)))); + } + + void instanceResetUserInterface() { + fHslider0 = FAUSTFLOAT(100.0f); + fHslider1 = FAUSTFLOAT(50.0f); + fHslider2 = FAUSTFLOAT(50.0f); + fHslider3 = FAUSTFLOAT(0.5f); + fHslider4 = FAUSTFLOAT(1.0f); + fHslider5 = FAUSTFLOAT(100.0f); + fHslider6 = FAUSTFLOAT(0.0f); + fHslider7 = FAUSTFLOAT(10000.0f); + fHslider8 = FAUSTFLOAT(100.0f); + fHslider9 = FAUSTFLOAT(75.0f); + fHslider10 = FAUSTFLOAT(62.5f); + fHslider11 = FAUSTFLOAT(70.0f); + fHslider12 = FAUSTFLOAT(5500.0f); + } + + void instanceClear() { + for (int l0 = 0; (l0 < 2); l0 = (l0 + 1)) { + fRec0[l0] = 0.0f; + } + for (int l1 = 0; (l1 < 2); l1 = (l1 + 1)) { + fRec1[l1] = 0.0f; + } + for (int l2 = 0; (l2 < 2); l2 = (l2 + 1)) { + fRec10[l2] = 0.0f; + } + for (int l3 = 0; (l3 < 2); l3 = (l3 + 1)) { + fRec18[l3] = 0.0f; + } + for (int l5 = 0; (l5 < 2); l5 = (l5 + 1)) { + fRec21[l5] = 0.0f; + } + for (int l6 = 0; (l6 < 2); l6 = (l6 + 1)) { + fRec20[l6] = 0.0f; + } + for (int l7 = 0; (l7 < 2); l7 = (l7 + 1)) { + fRec14[l7] = 0.0f; + } + for (int l8 = 0; (l8 < 2); l8 = (l8 + 1)) { + fRec15[l8] = 0.0f; + } + for (int l9 = 0; (l9 < 2); l9 = (l9 + 1)) { + iRec16[l9] = 0; + } + for (int l10 = 0; (l10 < 2); l10 = (l10 + 1)) { + iRec17[l10] = 0; + } + for (int l11 = 0; (l11 < 2); l11 = (l11 + 1)) { + fRec32[l11] = 0.0f; + } + IOTA = 0; + for (int l12 = 0; (l12 < 131072); l12 = (l12 + 1)) { + fVec0[l12] = 0.0f; + } + for (int l13 = 0; (l13 < 2); l13 = (l13 + 1)) { + fRec33[l13] = 0.0f; + } + for (int l14 = 0; (l14 < 2); l14 = (l14 + 1)) { + fRec34[l14] = 0.0f; + } + for (int l15 = 0; (l15 < 2); l15 = (l15 + 1)) { + fRec31[l15] = 0.0f; + } + for (int l16 = 0; (l16 < 2); l16 = (l16 + 1)) { + fRec35[l16] = 0.0f; + } + for (int l17 = 0; (l17 < 2); l17 = (l17 + 1)) { + fRec30[l17] = 0.0f; + } + for (int l18 = 0; (l18 < 2); l18 = (l18 + 1)) { + fRec36[l18] = 0.0f; + } + for (int l19 = 0; (l19 < 1024); l19 = (l19 + 1)) { + fVec1[l19] = 0.0f; + } + for (int l20 = 0; (l20 < 2); l20 = (l20 + 1)) { + fRec28[l20] = 0.0f; + } + for (int l21 = 0; (l21 < 1024); l21 = (l21 + 1)) { + fVec2[l21] = 0.0f; + } + for (int l22 = 0; (l22 < 2); l22 = (l22 + 1)) { + fRec26[l22] = 0.0f; + } + for (int l23 = 0; (l23 < 2); l23 = (l23 + 1)) { + fRec37[l23] = 0.0f; + } + for (int l24 = 0; (l24 < 4096); l24 = (l24 + 1)) { + fVec3[l24] = 0.0f; + } + for (int l25 = 0; (l25 < 2); l25 = (l25 + 1)) { + fRec24[l25] = 0.0f; + } + for (int l26 = 0; (l26 < 2048); l26 = (l26 + 1)) { + fVec4[l26] = 0.0f; + } + for (int l27 = 0; (l27 < 2); l27 = (l27 + 1)) { + fRec22[l27] = 0.0f; + } + for (int l28 = 0; (l28 < 2); l28 = (l28 + 1)) { + fRec38[l28] = 0.0f; + } + for (int l29 = 0; (l29 < 131072); l29 = (l29 + 1)) { + fVec5[l29] = 0.0f; + } + for (int l30 = 0; (l30 < 2); l30 = (l30 + 1)) { + fRec12[l30] = 0.0f; + } + for (int l31 = 0; (l31 < 32768); l31 = (l31 + 1)) { + fVec6[l31] = 0.0f; + } + for (int l32 = 0; (l32 < 2); l32 = (l32 + 1)) { + fRec39[l32] = 0.0f; + } + for (int l33 = 0; (l33 < 2); l33 = (l33 + 1)) { + fRec11[l33] = 0.0f; + } + for (int l34 = 0; (l34 < 32768); l34 = (l34 + 1)) { + fVec7[l34] = 0.0f; + } + for (int l35 = 0; (l35 < 2); l35 = (l35 + 1)) { + fRec8[l35] = 0.0f; + } + for (int l36 = 0; (l36 < 32768); l36 = (l36 + 1)) { + fRec2[l36] = 0.0f; + } + for (int l37 = 0; (l37 < 16384); l37 = (l37 + 1)) { + fRec3[l37] = 0.0f; + } + for (int l38 = 0; (l38 < 32768); l38 = (l38 + 1)) { + fRec4[l38] = 0.0f; + } + for (int l39 = 0; (l39 < 2); l39 = (l39 + 1)) { + fRec45[l39] = 0.0f; + } + for (int l40 = 0; (l40 < 2); l40 = (l40 + 1)) { + fRec46[l40] = 0.0f; + } + for (int l41 = 0; (l41 < 2); l41 = (l41 + 1)) { + iRec47[l41] = 0; + } + for (int l42 = 0; (l42 < 2); l42 = (l42 + 1)) { + iRec48[l42] = 0; + } + for (int l43 = 0; (l43 < 131072); l43 = (l43 + 1)) { + fVec8[l43] = 0.0f; + } + for (int l44 = 0; (l44 < 2); l44 = (l44 + 1)) { + fRec58[l44] = 0.0f; + } + for (int l45 = 0; (l45 < 2); l45 = (l45 + 1)) { + fRec57[l45] = 0.0f; + } + for (int l46 = 0; (l46 < 1024); l46 = (l46 + 1)) { + fVec9[l46] = 0.0f; + } + for (int l47 = 0; (l47 < 2); l47 = (l47 + 1)) { + fRec55[l47] = 0.0f; + } + for (int l48 = 0; (l48 < 1024); l48 = (l48 + 1)) { + fVec10[l48] = 0.0f; + } + for (int l49 = 0; (l49 < 2); l49 = (l49 + 1)) { + fRec53[l49] = 0.0f; + } + for (int l50 = 0; (l50 < 4096); l50 = (l50 + 1)) { + fVec11[l50] = 0.0f; + } + for (int l51 = 0; (l51 < 2); l51 = (l51 + 1)) { + fRec51[l51] = 0.0f; + } + for (int l52 = 0; (l52 < 2048); l52 = (l52 + 1)) { + fVec12[l52] = 0.0f; + } + for (int l53 = 0; (l53 < 2); l53 = (l53 + 1)) { + fRec49[l53] = 0.0f; + } + for (int l54 = 0; (l54 < 131072); l54 = (l54 + 1)) { + fVec13[l54] = 0.0f; + } + for (int l55 = 0; (l55 < 2); l55 = (l55 + 1)) { + fRec43[l55] = 0.0f; + } + for (int l56 = 0; (l56 < 32768); l56 = (l56 + 1)) { + fVec14[l56] = 0.0f; + } + for (int l57 = 0; (l57 < 2); l57 = (l57 + 1)) { + fRec42[l57] = 0.0f; + } + for (int l58 = 0; (l58 < 16384); l58 = (l58 + 1)) { + fVec15[l58] = 0.0f; + } + for (int l59 = 0; (l59 < 2); l59 = (l59 + 1)) { + fRec40[l59] = 0.0f; + } + for (int l60 = 0; (l60 < 32768); l60 = (l60 + 1)) { + fRec5[l60] = 0.0f; + } + for (int l61 = 0; (l61 < 8192); l61 = (l61 + 1)) { + fRec6[l61] = 0.0f; + } + for (int l62 = 0; (l62 < 32768); l62 = (l62 + 1)) { + fRec7[l62] = 0.0f; + } + } + + void init(int sample_rate) { + classInit(sample_rate); + instanceInit(sample_rate); + } + void instanceInit(int sample_rate) { + instanceConstants(sample_rate); + instanceResetUserInterface(); + instanceClear(); + } + + faustFverb* clone() { + return new faustFverb(); + } + + int getSampleRate() { + return fSampleRate; + } + + void buildUserInterface() { + } + + void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { + FAUSTFLOAT* input0 = inputs[0]; + FAUSTFLOAT* input1 = inputs[1]; + FAUSTFLOAT* output0 = outputs[0]; + FAUSTFLOAT* output1 = outputs[1]; + float fSlow0 = (9.99999975e-06f * float(fHslider0)); + float fSlow1 = (9.99999975e-06f * float(fHslider1)); + float fSlow2 = (9.99999975e-06f * float(fHslider2)); + float fSlow3 = (9.99999997e-07f * float(fHslider3)); + float fSlow4 = (0.00100000005f * float(fHslider4)); + float fSlow5 = (9.99999975e-06f * float(fHslider5)); + float fSlow6 = (9.99999997e-07f * float(fHslider6)); + float fSlow7 = (0.00100000005f * std::exp((fConst1 * (0.0f - (6.28318548f * float(fHslider7)))))); + float fSlow8 = (0.00100000005f * std::exp((fConst1 * (0.0f - (6.28318548f * float(fHslider8)))))); + float fSlow9 = (9.99999975e-06f * float(fHslider9)); + float fSlow10 = (9.99999975e-06f * float(fHslider10)); + float fSlow11 = (9.99999975e-06f * float(fHslider11)); + float fSlow12 = (0.00100000005f * std::exp((fConst1 * (0.0f - (6.28318548f * float(fHslider12)))))); + for (int i = 0; (i < count); i = (i + 1)) { + float fTemp0 = float(input0[i]); + float fTemp1 = float(input1[i]); + fRec0[0] = (fSlow0 + (0.999000013f * fRec0[1])); + fRec1[0] = (fSlow1 + (0.999000013f * fRec1[1])); + fRec10[0] = (fSlow2 + (0.999000013f * fRec10[1])); + float fTemp2 = std::min(0.5f, std::max(0.25f, (fRec10[0] + 0.150000006f))); + fRec18[0] = (fSlow3 + (0.999000013f * fRec18[1])); + fRec21[0] = (fSlow4 + (0.999000013f * fRec21[1])); + float fTemp3 = (fRec20[1] + (fConst1 * fRec21[0])); + fRec20[0] = (fTemp3 - float(int(fTemp3))); + int iTemp4 = (int((fConst0 * ((fRec18[0] * ftbl0faustFverbSIG0[int((65536.0f * (fRec20[0] + (0.25f - float(int((fRec20[0] + 0.25f)))))))]) + 0.0305097271f))) + -1); + float fTemp5 = ((fRec14[1] != 0.0f) ? (((fRec15[1] > 0.0f) & (fRec15[1] < 1.0f)) ? fRec14[1] : 0.0f) : (((fRec15[1] == 0.0f) & (iTemp4 != iRec16[1])) ? fConst2 : (((fRec15[1] == 1.0f) & (iTemp4 != iRec17[1])) ? fConst3 : 0.0f))); + fRec14[0] = fTemp5; + fRec15[0] = std::max(0.0f, std::min(1.0f, (fRec15[1] + fTemp5))); + iRec16[0] = (((fRec15[1] >= 1.0f) & (iRec17[1] != iTemp4)) ? iTemp4 : iRec16[1]); + iRec17[0] = (((fRec15[1] <= 0.0f) & (iRec16[1] != iTemp4)) ? iTemp4 : iRec17[1]); + fRec32[0] = (fSlow5 + (0.999000013f * fRec32[1])); + fVec0[(IOTA & 131071)] = (fTemp1 * fRec32[0]); + fRec33[0] = (fSlow6 + (0.999000013f * fRec33[1])); + int iTemp6 = std::min(65536, std::max(0, int((fConst0 * fRec33[0])))); + fRec34[0] = (fSlow7 + (0.999000013f * fRec34[1])); + fRec31[0] = (fVec0[((IOTA - iTemp6) & 131071)] + (fRec34[0] * fRec31[1])); + float fTemp7 = (1.0f - fRec34[0]); + fRec35[0] = (fSlow8 + (0.999000013f * fRec35[1])); + fRec30[0] = ((fRec31[0] * fTemp7) + (fRec35[0] * fRec30[1])); + float fTemp8 = (fRec35[0] + 1.0f); + float fTemp9 = (0.0f - (0.5f * fTemp8)); + fRec36[0] = (fSlow9 + (0.999000013f * fRec36[1])); + float fTemp10 = (((0.5f * (fRec30[0] * fTemp8)) + (fRec30[1] * fTemp9)) - (fRec36[0] * fRec28[1])); + fVec1[(IOTA & 1023)] = fTemp10; + fRec28[0] = fVec1[((IOTA - iConst4) & 1023)]; + float fRec29 = (fRec36[0] * fTemp10); + float fTemp11 = ((fRec29 + fRec28[1]) - (fRec36[0] * fRec26[1])); + fVec2[(IOTA & 1023)] = fTemp11; + fRec26[0] = fVec2[((IOTA - iConst5) & 1023)]; + float fRec27 = (fRec36[0] * fTemp11); + fRec37[0] = (fSlow10 + (0.999000013f * fRec37[1])); + float fTemp12 = ((fRec27 + fRec26[1]) - (fRec37[0] * fRec24[1])); + fVec3[(IOTA & 4095)] = fTemp12; + fRec24[0] = fVec3[((IOTA - iConst6) & 4095)]; + float fRec25 = (fRec37[0] * fTemp12); + float fTemp13 = ((fRec25 + fRec24[1]) - (fRec37[0] * fRec22[1])); + fVec4[(IOTA & 2047)] = fTemp13; + fRec22[0] = fVec4[((IOTA - iConst7) & 2047)]; + float fRec23 = (fRec37[0] * fTemp13); + fRec38[0] = (fSlow11 + (0.999000013f * fRec38[1])); + float fTemp14 = (fRec22[1] + ((fRec10[0] * fRec5[((IOTA - iConst8) & 32767)]) + (fRec23 + (fRec38[0] * fRec12[1])))); + fVec5[(IOTA & 131071)] = fTemp14; + fRec12[0] = (((1.0f - fRec15[0]) * fVec5[((IOTA - std::min(65536, std::max(0, iRec16[0]))) & 131071)]) + (fRec15[0] * fVec5[((IOTA - std::min(65536, std::max(0, iRec17[0]))) & 131071)])); + float fRec13 = (0.0f - (fRec38[0] * fTemp14)); + float fTemp15 = (fRec13 + fRec12[1]); + fVec6[(IOTA & 32767)] = fTemp15; + fRec39[0] = (fSlow12 + (0.999000013f * fRec39[1])); + fRec11[0] = (fVec6[((IOTA - iConst9) & 32767)] + (fRec39[0] * fRec11[1])); + float fTemp16 = (1.0f - fRec39[0]); + float fTemp17 = ((fTemp2 * fRec8[1]) + ((fRec10[0] * fRec11[0]) * fTemp16)); + fVec7[(IOTA & 32767)] = fTemp17; + fRec8[0] = fVec7[((IOTA - iConst10) & 32767)]; + float fRec9 = (0.0f - (fTemp2 * fTemp17)); + fRec2[(IOTA & 32767)] = (fRec9 + fRec8[1]); + fRec3[(IOTA & 16383)] = (fRec11[0] * fTemp16); + fRec4[(IOTA & 32767)] = fTemp15; + int iTemp18 = (int((fConst0 * ((fRec18[0] * ftbl0faustFverbSIG0[int((65536.0f * fRec20[0]))]) + 0.025603978f))) + -1); + float fTemp19 = ((fRec45[1] != 0.0f) ? (((fRec46[1] > 0.0f) & (fRec46[1] < 1.0f)) ? fRec45[1] : 0.0f) : (((fRec46[1] == 0.0f) & (iTemp18 != iRec47[1])) ? fConst2 : (((fRec46[1] == 1.0f) & (iTemp18 != iRec48[1])) ? fConst3 : 0.0f))); + fRec45[0] = fTemp19; + fRec46[0] = std::max(0.0f, std::min(1.0f, (fRec46[1] + fTemp19))); + iRec47[0] = (((fRec46[1] >= 1.0f) & (iRec48[1] != iTemp18)) ? iTemp18 : iRec47[1]); + iRec48[0] = (((fRec46[1] <= 0.0f) & (iRec47[1] != iTemp18)) ? iTemp18 : iRec48[1]); + fVec8[(IOTA & 131071)] = (fTemp0 * fRec32[0]); + fRec58[0] = (fVec8[((IOTA - iTemp6) & 131071)] + (fRec34[0] * fRec58[1])); + fRec57[0] = ((fTemp7 * fRec58[0]) + (fRec35[0] * fRec57[1])); + float fTemp20 = (((0.5f * (fRec57[0] * fTemp8)) + (fTemp9 * fRec57[1])) - (fRec36[0] * fRec55[1])); + fVec9[(IOTA & 1023)] = fTemp20; + fRec55[0] = fVec9[((IOTA - iConst11) & 1023)]; + float fRec56 = (fRec36[0] * fTemp20); + float fTemp21 = ((fRec56 + fRec55[1]) - (fRec36[0] * fRec53[1])); + fVec10[(IOTA & 1023)] = fTemp21; + fRec53[0] = fVec10[((IOTA - iConst12) & 1023)]; + float fRec54 = (fRec36[0] * fTemp21); + float fTemp22 = ((fRec54 + fRec53[1]) - (fRec37[0] * fRec51[1])); + fVec11[(IOTA & 4095)] = fTemp22; + fRec51[0] = fVec11[((IOTA - iConst13) & 4095)]; + float fRec52 = (fRec37[0] * fTemp22); + float fTemp23 = ((fRec52 + fRec51[1]) - (fRec37[0] * fRec49[1])); + fVec12[(IOTA & 2047)] = fTemp23; + fRec49[0] = fVec12[((IOTA - iConst14) & 2047)]; + float fRec50 = (fRec37[0] * fTemp23); + float fTemp24 = (fRec49[1] + ((fRec10[0] * fRec2[((IOTA - iConst15) & 32767)]) + (fRec50 + (fRec38[0] * fRec43[1])))); + fVec13[(IOTA & 131071)] = fTemp24; + fRec43[0] = (((1.0f - fRec46[0]) * fVec13[((IOTA - std::min(65536, std::max(0, iRec47[0]))) & 131071)]) + (fRec46[0] * fVec13[((IOTA - std::min(65536, std::max(0, iRec48[0]))) & 131071)])); + float fRec44 = (0.0f - (fRec38[0] * fTemp24)); + float fTemp25 = (fRec44 + fRec43[1]); + fVec14[(IOTA & 32767)] = fTemp25; + fRec42[0] = (fVec14[((IOTA - iConst16) & 32767)] + (fRec39[0] * fRec42[1])); + float fTemp26 = ((fTemp2 * fRec40[1]) + ((fRec10[0] * fTemp16) * fRec42[0])); + fVec15[(IOTA & 16383)] = fTemp26; + fRec40[0] = fVec15[((IOTA - iConst17) & 16383)]; + float fRec41 = (0.0f - (fTemp2 * fTemp26)); + fRec5[(IOTA & 32767)] = (fRec41 + fRec40[1]); + fRec6[(IOTA & 8191)] = (fTemp16 * fRec42[0]); + fRec7[(IOTA & 32767)] = fTemp25; + output0[i] = FAUSTFLOAT(((fTemp0 * fRec0[0]) + (0.600000024f * (fRec1[0] * (((fRec4[((IOTA - iConst18) & 32767)] + fRec4[((IOTA - iConst19) & 32767)]) + fRec2[((IOTA - iConst20) & 32767)]) - (((fRec3[((IOTA - iConst21) & 16383)] + fRec7[((IOTA - iConst22) & 32767)]) + fRec6[((IOTA - iConst23) & 8191)]) + fRec5[((IOTA - iConst24) & 32767)])))))); + output1[i] = FAUSTFLOAT(((fTemp1 * fRec0[0]) + (0.600000024f * (fRec1[0] * (((fRec7[((IOTA - iConst25) & 32767)] + fRec7[((IOTA - iConst26) & 32767)]) + fRec5[((IOTA - iConst27) & 32767)]) - (((fRec6[((IOTA - iConst28) & 8191)] + fRec4[((IOTA - iConst29) & 32767)]) + fRec3[((IOTA - iConst30) & 16383)]) + fRec2[((IOTA - iConst31) & 32767)])))))); + fRec0[1] = fRec0[0]; + fRec1[1] = fRec1[0]; + fRec10[1] = fRec10[0]; + fRec18[1] = fRec18[0]; + fRec21[1] = fRec21[0]; + fRec20[1] = fRec20[0]; + fRec14[1] = fRec14[0]; + fRec15[1] = fRec15[0]; + iRec16[1] = iRec16[0]; + iRec17[1] = iRec17[0]; + fRec32[1] = fRec32[0]; + IOTA = (IOTA + 1); + fRec33[1] = fRec33[0]; + fRec34[1] = fRec34[0]; + fRec31[1] = fRec31[0]; + fRec35[1] = fRec35[0]; + fRec30[1] = fRec30[0]; + fRec36[1] = fRec36[0]; + fRec28[1] = fRec28[0]; + fRec26[1] = fRec26[0]; + fRec37[1] = fRec37[0]; + fRec24[1] = fRec24[0]; + fRec22[1] = fRec22[0]; + fRec38[1] = fRec38[0]; + fRec12[1] = fRec12[0]; + fRec39[1] = fRec39[0]; + fRec11[1] = fRec11[0]; + fRec8[1] = fRec8[0]; + fRec45[1] = fRec45[0]; + fRec46[1] = fRec46[0]; + iRec47[1] = iRec47[0]; + iRec48[1] = iRec48[0]; + fRec58[1] = fRec58[0]; + fRec57[1] = fRec57[0]; + fRec55[1] = fRec55[0]; + fRec53[1] = fRec53[0]; + fRec51[1] = fRec51[0]; + fRec49[1] = fRec49[0]; + fRec43[1] = fRec43[0]; + fRec42[1] = fRec42[0]; + fRec40[1] = fRec40[0]; + } + } + +}; + +#ifdef FAUST_UIMACROS + + + + #define FAUST_LIST_ACTIVES(p) \ + p(HORIZONTALSLIDER, Predelay, "Predelay", fHslider6, 0.0f, 0.0f, 300.0f, 1.0f) \ + p(HORIZONTALSLIDER, Input_amount, "Input amount", fHslider5, 100.0f, 0.0f, 100.0f, 0.01f) \ + p(HORIZONTALSLIDER, Input_low_pass_cutoff, "Input low pass cutoff", fHslider7, 10000.0f, 1.0f, 20000.0f, 1.0f) \ + p(HORIZONTALSLIDER, Input_high_pass_cutoff, "Input high pass cutoff", fHslider8, 100.0f, 1.0f, 1000.0f, 1.0f) \ + p(HORIZONTALSLIDER, Input_diffusion_1, "Input diffusion 1", fHslider9, 75.0f, 0.0f, 100.0f, 0.01f) \ + p(HORIZONTALSLIDER, Input_diffusion_2, "Input diffusion 2", fHslider10, 62.5f, 0.0f, 100.0f, 0.01f) \ + p(HORIZONTALSLIDER, Tail_density, "Tail density", fHslider11, 70.0f, 0.0f, 100.0f, 0.01f) \ + p(HORIZONTALSLIDER, Decay, "Decay", fHslider2, 50.0f, 0.0f, 100.0f, 0.01f) \ + p(HORIZONTALSLIDER, Damping, "Damping", fHslider12, 5500.0f, 10.0f, 20000.0f, 1.0f) \ + p(HORIZONTALSLIDER, Modulator_frequency, "Modulator frequency", fHslider4, 1.0f, 0.01f, 4.0f, 0.01f) \ + p(HORIZONTALSLIDER, Modulator_depth, "Modulator depth", fHslider3, 0.5f, 0.0f, 10.0f, 0.10000000000000001f) \ + p(HORIZONTALSLIDER, Dry, "Dry", fHslider0, 100.0f, 0.0f, 100.0f, 0.01f) \ + p(HORIZONTALSLIDER, Wet, "Wet", fHslider1, 50.0f, 0.0f, 100.0f, 0.01f) \ + + #define FAUST_LIST_PASSIVES(p) \ + +#endif + +#endif From c314131bff1e84ac88bfeedea54ec8e09867584f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 30 Jul 2020 11:55:04 +0200 Subject: [PATCH 027/445] Accept higher values for predelay --- src/sfizz/effects/Fverb.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/effects/Fverb.cpp b/src/sfizz/effects/Fverb.cpp index 9dbf04d6..b776f862 100644 --- a/src/sfizz/effects/Fverb.cpp +++ b/src/sfizz/effects/Fverb.cpp @@ -212,7 +212,7 @@ namespace fx { setValueFromOpcode(opc, size, {0.0f, 100.0f}); break; case hash("reverb_predelay"): - setValueFromOpcode(opc, predelay, {0.0f, 1.0f}); + setValueFromOpcode(opc, predelay, {0.0f, 10.0f}); break; case hash("reverb_tone"): setValueFromOpcode(opc, tone, {0.0f, 100.0f}); From 3ebc14c894d8b55a6643c3121f641e86d1285ab2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 16:15:19 +0200 Subject: [PATCH 028/445] Add reverb preset: chamber --- src/sfizz/effects/Fverb.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/sfizz/effects/Fverb.cpp b/src/sfizz/effects/Fverb.cpp index b776f862..71e73efa 100644 --- a/src/sfizz/effects/Fverb.cpp +++ b/src/sfizz/effects/Fverb.cpp @@ -61,6 +61,7 @@ namespace fx { static const Profile largeHall; static const Profile midHall; static const Profile smallHall; + static const Profile chamber; static double lpfCutoff(double x) { @@ -118,6 +119,14 @@ namespace fx { 100, // dry 60, // wet }; + const Fverb::Impl::Profile Fverb::Impl::chamber { + 80, // tail density + 95, // decay at max size + 0.85, // modulation frequency + 1.5, // modulation depth + 100, // dry + 60, // wet + }; /// Fverb::Fverb() @@ -197,6 +206,8 @@ namespace fx { profile = &Impl::midHall; else if (value == "small_hall") profile = &Impl::smallHall; + else if (value == "chamber") + profile = &Impl::chamber; } break; case hash("reverb_dry"): From 5928218b62903528993e0f0080de63eb5a086a96 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 16:44:39 +0200 Subject: [PATCH 029/445] Use the spinlock mutex in FilePool --- src/sfizz/FilePool.cpp | 4 ++-- src/sfizz/FilePool.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 9bec98ef..8c474e9f 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -361,7 +361,7 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept void sfz::FilePool::tryToClearPromises() { - const std::lock_guard promiseLock { promiseGuard }; + const std::lock_guard promiseLock { promiseGuard }; for (auto& promise: promisesToClear) { if (promise->dataStatus != FilePromise::DataStatus::Wait) @@ -442,7 +442,7 @@ void sfz::FilePool::clear() void sfz::FilePool::cleanupPromises() noexcept { - const std::unique_lock lock { promiseGuard, std::try_to_lock }; + const std::unique_lock lock { promiseGuard, std::try_to_lock }; if (!lock.owns_lock()) return; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 2547591f..cbec5f8f 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -32,6 +32,7 @@ #include "AudioSpan.h" #include "FileId.h" #include "SIMDHelpers.h" +#include "utility/SpinMutex.h" #include "ghc/fs_std.hpp" #include #include @@ -288,7 +289,7 @@ private: std::vector emptyPromises; std::vector temporaryFilePromises; std::vector promisesToClear; - std::mutex promiseGuard; + SpinMutex promiseGuard; // Preloaded data absl::flat_hash_map preloadedFiles; From 76654d56eacd16c49c03c39f5d84c54c9a94eef4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 16:45:56 +0200 Subject: [PATCH 030/445] Use the spinlock mutex in FilterPool --- src/sfizz/FilterPool.cpp | 4 ++-- src/sfizz/FilterPool.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 164b3ea8..70811320 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -109,7 +109,7 @@ sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, unsigned numChannels, int noteNumber, float velocity) { - const std::unique_lock lock { filterGuard, std::try_to_lock }; + const std::unique_lock lock { filterGuard, std::try_to_lock }; if (!lock.owns_lock()) return {}; @@ -133,7 +133,7 @@ size_t sfz::FilterPool::getActiveFilters() const size_t sfz::FilterPool::setNumFilters(size_t numFilters) { - const std::lock_guard filterLock { filterGuard }; + const std::lock_guard filterLock { filterGuard }; swapAndPopAll(filters, [](sfz::FilterHolderPtr& filter) { return filter.use_count() == 1; }); diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index 36db8f51..197eb8ad 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -3,6 +3,7 @@ #include "FilterDescription.h" #include "MidiState.h" #include "Defaults.h" +#include "utility/SpinMutex.h" #include #include #include @@ -120,7 +121,7 @@ public: */ void setSampleRate(float sampleRate); private: - std::mutex filterGuard; + SpinMutex filterGuard; float sampleRate { config::defaultSampleRate }; const MidiState& midiState; std::vector filters; From f3e37cc713368b2b30bd7d9fdacaac6b188c017a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 16:46:56 +0200 Subject: [PATCH 031/445] Use the spinlock mutex in EQPool --- src/sfizz/EQPool.cpp | 4 ++-- src/sfizz/EQPool.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index 6d3d75b2..cc608a12 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -108,7 +108,7 @@ sfz::EQPool::EQPool(const MidiState& state, int numEQs) sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, unsigned numChannels, float velocity) { - const std::unique_lock lock { eqGuard, std::try_to_lock }; + const std::unique_lock lock { eqGuard, std::try_to_lock }; if (!lock.owns_lock()) return {}; @@ -132,7 +132,7 @@ size_t sfz::EQPool::getActiveEQs() const size_t sfz::EQPool::setnumEQs(size_t numEQs) { - const std::lock_guard eqLock { eqGuard }; + const std::lock_guard eqLock { eqGuard }; swapAndPopAll(eqs, [](sfz::EQHolderPtr& eq) { return eq.use_count() == 1; }); diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index 7c70af22..d1f7cebb 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -2,6 +2,7 @@ #include "SfzFilter.h" #include "EQDescription.h" #include "MidiState.h" +#include "utility/SpinMutex.h" #include #include #include @@ -115,7 +116,7 @@ public: */ void setSampleRate(float sampleRate); private: - std::mutex eqGuard; + SpinMutex eqGuard; float sampleRate { config::defaultSampleRate }; const MidiState& midiState; std::vector eqs; From c39a50ee92ab85b410cdd7b6360e330d19c1909d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 18 Jul 2020 11:37:29 +0200 Subject: [PATCH 032/445] Voice stealing tweaks --- src/sfizz/Synth.cpp | 23 +++++++++++++++-------- src/sfizz/Voice.cpp | 3 ++- src/sfizz/Voice.h | 27 +++++++++++++++------------ src/sfizz/VoiceStealing.cpp | 12 ++++++++---- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e5f091a8..dc2e0596 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -680,6 +680,9 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept { const Region* region = voice.getRegion(); + if (region == nullptr) + return; + voice.renderBlock(tempSpan); for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { if (auto& bus = effectBuses[i]) { @@ -936,8 +939,12 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } render: + // For some reason we did not find a voice to use. + // This is a degraded case but we'll just drop the note on. + if (selectedVoice == nullptr) + continue; + // Kill voice if necessary, pre-rendering it into the output buffers - ASSERT(selectedVoice); if (!selectedVoice->isFree()) { auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); SisterVoiceRing::applyToRing(selectedVoice, [&] (Voice* v) { @@ -1295,22 +1302,22 @@ void sfz::Synth::resetVoices(int numVoices) voices.clear(); voices.reserve(numVoices); - for (int i = 0; i < numVoices; ++i) { - auto voice = absl::make_unique(i, resources); - voice->setStateListener(this); - voices.emplace_back(std::move(voice)); - } - voiceViewArray.clear(); voiceViewArray.reserve(numVoices); regionPolyphonyArray.clear(); regionPolyphonyArray.reserve(numVoices); + for (int i = 0; i < numVoices; ++i) { + auto voice = absl::make_unique(i, resources); + voice->setStateListener(this); + voiceViewArray.push_back(voice.get()); + voices.emplace_back(std::move(voice)); + } + for (auto& voice : voices) { voice->setSampleRate(this->sampleRate); voice->setSamplesPerBlock(this->samplesPerBlock); - voiceViewArray.push_back(voice.get()); } this->numVoices = numVoices; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 771900d1..910fed95 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -270,7 +270,8 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept ASSERT(static_cast(buffer.getNumFrames()) <= samplesPerBlock); buffer.fill(0.0f); - ASSERT(region != nullptr); + if (region == nullptr) + return; const auto delay = min(static_cast(initialDelay), buffer.getNumFrames()); auto delayed_buffer = buffer.subspan(delay); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 74fa080c..b00b2a18 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -494,28 +494,31 @@ private: inline bool sisterVoices(const Voice* lhs, const Voice* rhs) { - return lhs->getAge() == rhs->getAge() - && lhs->getTriggerNumber() == rhs->getTriggerNumber() - && lhs->getTriggerValue() == rhs->getTriggerValue() - && lhs->getTriggerType() == rhs->getTriggerType(); + if (lhs->getAge() != rhs->getAge()) + return false; + + if (lhs->getTriggerNumber() != rhs->getTriggerNumber()) + return false; + + if (lhs->getTriggerValue() != rhs->getTriggerValue()) + return false; + + if (lhs->getTriggerType() != rhs->getTriggerType()) + return false; + + return true; } inline bool voiceOrdering(const Voice* lhs, const Voice* rhs) { if (lhs->getAge() > rhs->getAge()) return true; - if (lhs->getAge() < rhs->getAge()) - return false; - if (lhs->getTriggerNumber() > rhs->getTriggerNumber()) - return true; if (lhs->getTriggerNumber() < rhs->getTriggerNumber()) - return false; - - if (lhs->getTriggerValue() > rhs->getTriggerValue()) return true; + if (lhs->getTriggerValue() < rhs->getTriggerValue()) - return false; + return true; if (lhs->getTriggerType() > rhs->getTriggerType()) return true; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index b4eec3aa..9cb88bb8 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -8,7 +8,7 @@ sfz::VoiceStealing::VoiceStealing() sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept { // Start of the voice stealing algorithm - absl::c_sort(voices, voiceOrdering); + absl::c_stable_sort(voices, voiceOrdering); const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { return sum + v->getAverageEnvelope(); @@ -21,9 +21,8 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept // This is not perfect because pad-type voices will take a long time to output // their sound, but it's reasonable for sounds with a quick attack and longer // release. - const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; - // This needs to be positive - ASSERT(ageThreshold >= 0); + const auto ageThreshold = + static_cast(voices.front()->getAge() * config::stealingAgeCoeff) + 1; Voice* returnedVoice = voices.front(); unsigned idx = 0; @@ -49,5 +48,10 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept do { idx++; } while (idx < voices.size() && sisterVoices(ref, voices[idx])); } + + // Guard for future changes: voices with age 0 just started; don't kill those. + if (returnedVoice->getAge() == 0) + return {}; + return returnedVoice; } From 70cf1c0be3b4a68e6042057d9256cdcfbe363c9e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 7 Aug 2020 11:19:33 +0200 Subject: [PATCH 033/445] Assert rather than return --- src/sfizz/Synth.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index dc2e0596..6081ba2b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -680,8 +680,7 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept { const Region* region = voice.getRegion(); - if (region == nullptr) - return; + ASSERT(region != nullptr); voice.renderBlock(tempSpan); for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { From 0295bdb773c5f6c26ac7cc62732655bfd82835af Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 7 Aug 2020 11:19:41 +0200 Subject: [PATCH 034/445] Correct the ordering --- src/sfizz/Voice.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index b00b2a18..f567c5e6 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -511,17 +511,17 @@ inline bool sisterVoices(const Voice* lhs, const Voice* rhs) inline bool voiceOrdering(const Voice* lhs, const Voice* rhs) { - if (lhs->getAge() > rhs->getAge()) - return true; + if (lhs->getAge() != rhs->getAge()) + return lhs->getAge() > rhs->getAge(); - if (lhs->getTriggerNumber() < rhs->getTriggerNumber()) - return true; + if (lhs->getTriggerNumber() != rhs->getTriggerNumber()) + return lhs->getTriggerNumber() < rhs->getTriggerNumber(); - if (lhs->getTriggerValue() < rhs->getTriggerValue()) - return true; + if (lhs->getTriggerValue() != rhs->getTriggerValue()) + return lhs->getTriggerValue() < rhs->getTriggerValue(); - if (lhs->getTriggerType() > rhs->getTriggerType()) - return true; + if (lhs->getTriggerType() != rhs->getTriggerType()) + return lhs->getTriggerType() > rhs->getTriggerType(); return false; } From e5c1b98dc32ffa6532f301f9c88fca056753269e Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Sat, 8 Aug 2020 20:38:28 +0900 Subject: [PATCH 035/445] Fix build: update debug messages and assertions to match the latest API. --- src/sfizz/FilePool.cpp | 4 ++-- src/sfizz/Oversampler.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index d33689c0..a7f6e893 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -207,7 +207,7 @@ absl::optional sfz::FilePool::getFileInformation(const Fil const unsigned channels = reader->channels(); if (channels != 1 && channels != 2) { - DBG("[sfizz] Missing logic for " << sndFile.channels() << " channels, discarding sample " << fileId); + DBG("[sfizz] Missing logic for " << reader->channels() << " channels, discarding sample " << fileId); return {}; } @@ -398,7 +398,7 @@ void sfz::FilePool::loadingThread() noexcept std::error_code readError; AudioReaderPtr reader = createAudioReader(file, promise->fileId.isReverse(), &readError); if (readError) { - DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << readError.what()); + DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << readError.message()); promise->dataStatus = FilePromise::DataStatus::Error; continue; } diff --git a/src/sfizz/Oversampler.cpp b/src/sfizz/Oversampler.cpp index 67de1c30..8476ff34 100644 --- a/src/sfizz/Oversampler.cpp +++ b/src/sfizz/Oversampler.cpp @@ -139,8 +139,8 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s void sfz::Oversampler::stream(AudioReader& input, AudioSpan output, std::atomic* framesReady) { - ASSERT(output.getNumFrames() >= input.getNumFrames() * static_cast(factor)); - ASSERT(output.getNumChannels() == input.getNumChannels()); + ASSERT(output.getNumFrames() >= input.frames() * static_cast(factor)); + ASSERT(output.getNumChannels() == input.channels()); const auto numFrames = static_cast(input.frames()); const auto numChannels = input.channels(); From 1b794576e92647f7f8b57cc164a88f25212bfe67 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 8 Aug 2020 15:00:20 +0200 Subject: [PATCH 036/445] Check the validity of DBG() under release mode --- src/sfizz/Debug.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Debug.h b/src/sfizz/Debug.h index 2c2f270c..97c8bea8 100644 --- a/src/sfizz/Debug.h +++ b/src/sfizz/Debug.h @@ -64,10 +64,10 @@ #endif // Debug message -#if !defined(NDEBUG) || defined(SFIZZ_ENABLE_RELEASE_DBG) #include +#if !defined(NDEBUG) || defined(SFIZZ_ENABLE_RELEASE_DBG) #include #define DBG(ostream) do { std::cerr << std::fixed << std::setprecision(2) << ostream << '\n'; } while (0) #else -#define DBG(ostream) do {} while (0) +#define DBG(ostream) do { if (0) { std::cerr << ostream; } } while (0) #endif From 6086284efec6ad240cedf11a6dc51f687d149d78 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 19 Jul 2020 13:18:30 +0200 Subject: [PATCH 037/445] If the region spans multiple keys they can all fire on pedal up --- src/sfizz/Region.cpp | 38 ++++++++++++++++++++++--------- src/sfizz/Region.h | 4 +++- src/sfizz/Synth.cpp | 29 +++++++++++++++++------- tests/RegionT.cpp | 47 ++++++++++++++++++++++++++++++++------ tests/SynthT.cpp | 54 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 27 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 0a19625c..e03e50a7 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1031,24 +1031,45 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu keySwitched = true; } - const bool keyOk = keyRange.containsWithEnd(noteNumber); - if (!isSwitchedOn()) return false; if (!triggerOnNote) return false; + // Prerequisites + + const bool keyOk = keyRange.containsWithEnd(noteNumber); const bool velOk = velocityRange.containsWithEnd(velocity); const bool randOk = randRange.contains(randValue); - bool releaseTrigger = (trigger == SfzTrigger::release_key); + + if (!(velOk && keyOk && randOk)) + return false; + + // Release logic + + if (trigger == SfzTrigger::release_key) + return true; + if (trigger == SfzTrigger::release) { if (midiState.getCCValue(sustainCC) < sustainThreshold) - releaseTrigger = true; + return true; + + // If we reach this part, we're storing the notes to delay their release on CC up + // This is handled by the Synth object + + const auto sameNoteTest = [noteNumber](const std::pair& noteAndValue) { + return noteAndValue.first == noteNumber; + }; + + auto it = absl::c_find_if(delayedReleases, sameNoteTest); + if (it == delayedReleases.end()) + delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber)); else - noteIsOff = true; + it->second = velocity; } - return keyOk && velOk && randOk && releaseTrigger; + + return false; } bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept @@ -1062,11 +1083,6 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept if (!isSwitchedOn()) return false; - if (sustainCC == ccNumber && ccValue < sustainThreshold && noteIsOff) { - noteIsOff = false; - return true; - } - if (!triggerOnCC) return false; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 772ee399..366efd1f 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -376,6 +376,9 @@ struct Region { // Parent RegionSet* parent { nullptr }; + + // Started notes + std::vector> delayedReleases; private: const MidiState& midiState; bool keySwitched { true }; @@ -384,7 +387,6 @@ private: bool pitchSwitched { true }; bool bpmSwitched { true }; bool aftertouchSwitched { true }; - bool noteIsOff { false }; std::bitset ccSwitched; absl::string_view defaultPath { "" }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 6081ba2b..0ab5664b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -161,6 +161,9 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) lastRegion->parent = currentSet; currentSet->addRegion(lastRegion.get()); + // Adapt the size of the delayed releases to avoid allocating later on + lastRegion->delayedReleases.reserve(lastRegion->keyRange.length()); + regions.push_back(std::move(lastRegion)); } @@ -998,18 +1001,28 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept SisterVoiceRingBuilder ring; for (auto& region : ccActivationLists[ccNumber]) { - if (region->registerCC(ccNumber, normValue)) { + if (ccNumber == region->sustainCC) { + for (auto& note: region->delayedReleases) { + // FIXME: we really need to have some form of common method to find and start voices... + auto voice = findFreeVoice(); + if (voice == nullptr) + continue; + + voice->startVoice(region, delay, note.first, note.second, Voice::TriggerType::NoteOff); + + ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); + } + + region->delayedReleases.clear(); + } else if (region->registerCC(ccNumber, normValue)) { auto voice = findFreeVoice(); if (voice == nullptr) continue; - if (!region->triggerOnCC) { - // This is a sustain trigger - const auto replacedVelocity = resources.midiState.getNoteVelocity(region->pitchKeycenter); - voice->startVoice(region, delay, region->pitchKeycenter, replacedVelocity, Voice::TriggerType::NoteOff); - } else { - voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); - } + + voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); ring.addVoiceToRing(voice); RegionSet::registerVoiceInHierarchy(region, voice); diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index ecfea499..d98b6c7a 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1749,7 +1749,8 @@ TEST_CASE("[Region] Release and release key") { MidiState midiState; Region region { 0, midiState }; - region.parseOpcode({ "key", "63" }); + region.parseOpcode({ "lokey", "63" }); + region.parseOpcode({ "hikey", "65" }); region.parseOpcode({ "sample", "*sine" }); SECTION("Release key without sustain") { @@ -1765,9 +1766,8 @@ TEST_CASE("[Region] Release and release key") REQUIRE( !region.registerCC(64, 1.0f) ); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); - midiState.ccEvent(0, 64, 0.0f); - REQUIRE( !region.registerCC(64, 0.0f) ); } + SECTION("Release without sustain") { region.parseOpcode({ "trigger", "release" }); @@ -1775,20 +1775,53 @@ TEST_CASE("[Region] Release and release key") REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); } + SECTION("Release with sustain") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + midiState.noteOnEvent(0, 63, 0.5f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) ); + REQUIRE( region.delayedReleases.size() == 1 ); + std::vector> expected = { + { 63, 0.5f } + }; + REQUIRE( region.delayedReleases == expected ); } - SECTION("Release with sustain") + + SECTION("Release with sustain and 2 notes") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + midiState.noteOnEvent(0, 63, 0.5f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); - REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) ); - midiState.ccEvent(0, 64, 0.0f); - REQUIRE( region.registerCC(64, 0.0f) ); + midiState.noteOnEvent(0, 64, 0.6f); + REQUIRE( !region.registerNoteOn(64, 0.6f, 0.0f) ); + REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) ); + REQUIRE( !region.registerNoteOff(64, 0.2f, 0.0f) ); + REQUIRE( region.delayedReleases.size() == 2 ); + std::vector> expected = { + { 63, 0.5f }, + { 64, 0.6f } + }; + REQUIRE( region.delayedReleases == expected ); + } + + SECTION("Release with sustain and 2 notes but 1 outside") + { + region.parseOpcode({ "trigger", "release" }); + midiState.ccEvent(0, 64, 1.0f); + midiState.noteOnEvent(0, 63, 0.5f); + REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); + midiState.noteOnEvent(0, 66, 0.6f); + REQUIRE( !region.registerNoteOn(66, 0.6f, 0.0f) ); + REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) ); + REQUIRE( !region.registerNoteOff(66, 0.2f, 0.0f) ); + REQUIRE( region.delayedReleases.size() == 1 ); + std::vector> expected = { + { 63, 0.5f } + }; + REQUIRE( region.delayedReleases == expected ); } } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index ccd92b94..3a07437a 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -695,3 +695,57 @@ TEST_CASE("[Synth] Sustain threshold") synth.noteOff(0, 62, 85); REQUIRE( synth.getNumActiveVoices(true) == 2 ); } + +TEST_CASE("[Synth] Release (Multiple notes)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.noteOn(0, 63, 78); + synth.noteOn(0, 64, 34); + synth.cc(0, 64, 127); + synth.noteOff(0, 64, 0); + synth.noteOff(0, 63, 2); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 0 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices() == 3 ); +} + +TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*sine trigger=release_key + )"); + synth.noteOn(0, 62, 85); + synth.noteOn(0, 63, 78); + synth.noteOn(0, 64, 34); + synth.cc(0, 64, 127); + synth.noteOff(0, 64, 0); + synth.noteOff(0, 63, 2); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 3 ); +} + +TEST_CASE("[Synth] Release (Multiple notes, cleared the delayed voices after)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + synth.noteOn(0, 63, 78); + synth.noteOn(0, 64, 34); + synth.cc(0, 64, 127); + synth.noteOff(0, 64, 0); + synth.noteOff(0, 63, 2); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 0 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( synth.getRegionView(0)->delayedReleases.empty() ); +} From f631f7031d393600cd61cce68e1abeaccdb8ca20 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 7 Aug 2020 20:36:04 +0200 Subject: [PATCH 038/445] Update the tests --- tests/SynthT.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 3a07437a..17f2ee50 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -709,9 +709,9 @@ TEST_CASE("[Synth] Release (Multiple notes)") synth.noteOff(0, 64, 0); synth.noteOff(0, 63, 2); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); synth.cc(0, 64, 0); - REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); } TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") @@ -727,7 +727,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") synth.noteOff(0, 64, 0); synth.noteOff(0, 63, 2); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); } TEST_CASE("[Synth] Release (Multiple notes, cleared the delayed voices after)") @@ -744,8 +744,8 @@ TEST_CASE("[Synth] Release (Multiple notes, cleared the delayed voices after)") synth.noteOff(0, 64, 0); synth.noteOff(0, 63, 2); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); synth.cc(0, 64, 0); - REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); REQUIRE( synth.getRegionView(0)->delayedReleases.empty() ); } From 13752d80435001b3fab18fd979c821dc50e24ab2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 9 Aug 2020 05:19:16 +0200 Subject: [PATCH 039/445] Higher quality wavetable mipmaps --- src/sfizz/Wavetables.cpp | 77 +++++++++++++++++++++------------------- src/sfizz/Wavetables.h | 59 +++++++++++++++++------------- tests/DemoWavetables.cpp | 30 +++++++++++++--- tests/DemoWavetables.ui | 15 ++++++-- 4 files changed, 115 insertions(+), 66 deletions(-) diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index e294cfee..352f4e2d 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -283,65 +283,70 @@ const HarmonicProfile& HarmonicProfile::getSquare() } //------------------------------------------------------------------------------ -constexpr unsigned WavetableRange::countOctaves; -constexpr float WavetableRange::frequencyScaleFactor; +constexpr unsigned MipmapRange::N; +constexpr float MipmapRange::F1; +constexpr float MipmapRange::FN; -unsigned WavetableRange::getOctaveForFrequency(float f) +const float MipmapRange::K = 1.0 / F1; +const float MipmapRange::LogB = std::log(FN / F1) / (N - 1); + +const std::array MipmapRange::FrequencyToIndex = []() { - int oct = fp_exponent(frequencyScaleFactor * f); - return clamp(oct, 0, countOctaves - 1); -} + std::array table; -static const auto octaveForFrequencyTable = []() -{ - static constexpr unsigned N = 1024; - std::array table; - - constexpr double fmin = 1 / WavetableRange::frequencyScaleFactor; - constexpr double fmax = (1 << (WavetableRange::countOctaves - 1)) / WavetableRange::frequencyScaleFactor; - - for (unsigned i = 0; i < N; ++i) { - double f = fmin + (i * (1.0 / (N - 1))) * (fmax - fmin); - table[i] = std::log2(f * WavetableRange::frequencyScaleFactor); + for (unsigned i = 0; i < table.size() - 1; ++i) { + double r = i * (1.0 / (table.size() - 1)); + double f = F1 + r * (FN - F1); + double t = std::log(K * f) / LogB; + table[i] = clamp(t, 0, N - 1); } + // ensure the last element to be exact + table[table.size() - 1] = N - 1; return table; }(); -float WavetableRange::getFractionalOctaveForFrequency(float f) +float MipmapRange::getIndexForFrequency(float f) { - static constexpr unsigned N = octaveForFrequencyTable.size(); + static constexpr unsigned tableSize = FrequencyToIndex.size(); - constexpr double fmin = 1 / WavetableRange::frequencyScaleFactor; - constexpr double fmax = (1 << (WavetableRange::countOctaves - 1)) / WavetableRange::frequencyScaleFactor; + float pos = (f - F1) * ((tableSize - 1) / static_cast(FN - F1)); + pos = clamp(pos, 0, tableSize - 1); - float pos = (f - fmin) * ((N - 1) / static_cast(fmax - fmin)); int index1 = static_cast(pos); + int index2 = std::min(index1 + 1, tableSize - 1); float frac = pos - index1; - index1 = clamp(index1, 0, N - 1); - int index2 = std::min(index1 + 1, N - 1); - return (1.0f - frac) * octaveForFrequencyTable[index1] + - frac * octaveForFrequencyTable[index2]; + return (1.0f - frac) * FrequencyToIndex[index1] + + frac * FrequencyToIndex[index2]; } -WavetableRange WavetableRange::getRangeForOctave(int o) +const std::array MipmapRange::IndexToStartFrequency = []() { - WavetableRange range; + std::array table; + for (unsigned t = 0; t < N; ++t) + table[t] = std::exp(t * LogB) / K; + // end value for final table + table[N] = 22050.0; - Fraction mant = fp_mantissa(0.0f); - float k = 1.0f / frequencyScaleFactor; + return table; +}(); - range.minFrequency = k * fp_from_parts(0, o, 0); - range.maxFrequency = k * fp_from_parts(0, o, mant.den - 1); +MipmapRange MipmapRange::getRangeForIndex(int o) +{ + o = clamp(o, 0, N - 1); + + MipmapRange range; + range.minFrequency = IndexToStartFrequency[o]; + range.maxFrequency = IndexToStartFrequency[o + 1]; return range; } -WavetableRange WavetableRange::getRangeForFrequency(float f) +MipmapRange MipmapRange::getRangeForFrequency(float f) { - int oct = getOctaveForFrequency(f); - return getRangeForOctave(oct); + int index = static_cast(getIndexForFrequency(f)); + return getRangeForIndex(index); } //------------------------------------------------------------------------------ @@ -356,7 +361,7 @@ WavetableMulti WavetableMulti::createForHarmonicProfile( wm.allocateStorage(tableSize); for (unsigned m = 0; m < numTables; ++m) { - WavetableRange range = WavetableRange::getRangeForOctave(m); + MipmapRange range = MipmapRange::getRangeForIndex(m); double freq = range.maxFrequency; diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index e2f26cfb..66b56e34 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -11,6 +11,7 @@ #include "MathHelpers.h" #include #include +#include #include #include @@ -57,6 +58,11 @@ public: */ void setQuality(int q) { _quality = q; } + /** + Get the quality of this oscillator. (cf. `oscillator_quality`) + */ + int quality() const { return _quality; } + /** Compute a cycle of the oscillator, with constant frequency. */ @@ -117,36 +123,41 @@ public: }; /** - A helper to select ranges of a multi-sampled oscillator, according to the + A helper to select ranges of a mip-mapped wave, according to the frequency of an oscillator. The ranges are identified by octave numbers; not octaves in a musical sense, but as logarithmic divisions of the frequency range. */ -class WavetableRange { +class MipmapRange { public: float minFrequency = 0; float maxFrequency = 0; - static constexpr unsigned countOctaves = 10; - static constexpr float frequencyScaleFactor = 0.05; + // number of tables in the mipmap + static constexpr unsigned N = 24; + // start frequency of the first table in the mipmap + static constexpr float F1 = 20.0; + // start frequency of the last table in the mipmap + static constexpr float FN = 12000.0; - static unsigned getOctaveForFrequency(float f); - static float getFractionalOctaveForFrequency(float f); - static WavetableRange getRangeForOctave(int o); - static WavetableRange getRangeForFrequency(float f); + static float getIndexForFrequency(float f); + static MipmapRange getRangeForIndex(int o); + static MipmapRange getRangeForFrequency(float f); - // Note: using the frequency factor 0.05, octaves are as follows: - // octave 0: 20 Hz - 40 Hz - // octave 1: 40 Hz - 80 Hz - // octave 2: 80 Hz - 160 Hz - // octave 3: 160 Hz - 320 Hz - // octave 4: 320 Hz - 640 Hz - // octave 5: 640 Hz - 1280 Hz - // octave 6: 1280 Hz - 2560 Hz - // octave 7: 2560 Hz - 5120 Hz - // octave 8: 5120 Hz - 10240 Hz - // octave 9: 10240 Hz - 20480 Hz + // the frequency mapping of the mipmap is defined by formula: + // T(f) = log(k*f)/log(b) + // - T is the table number, converted to index by rounding down + // - f is the oscillation frequency + // - k and b are adjustment parameters according to constant parameters + // k = 1/F1 + // b = exp(log(FN/F1)/(N-1)) + + static const float K; + static const float LogB; + + static const std::array FrequencyToIndex; + static const std::array IndexToStartFrequency; }; /** @@ -159,7 +170,7 @@ public: unsigned tableSize() const { return _tableSize; } // number of tables in the multisample - static constexpr unsigned numTables() { return WavetableRange::countOctaves; } + static constexpr unsigned numTables() { return MipmapRange::N; } // get the N-th table in the multisample absl::Span getTable(unsigned index) const @@ -170,7 +181,7 @@ public: // get the table which is adequate for a given playback frequency absl::Span getTableForFrequency(float freq) const { - return getTable(WavetableRange::getOctaveForFrequency(freq)); + return getTable(MipmapRange::getIndexForFrequency(freq)); } // adjacent tables with interpolation factor between them @@ -186,15 +197,15 @@ public: DualTable dt; int index = static_cast(position); dt.delta = position - index; - dt.table1 = getTablePointer(clamp(index, 0, WavetableRange::countOctaves - 1)); - dt.table2 = getTablePointer(clamp(index + 1, 0, WavetableRange::countOctaves - 1)); + dt.table1 = getTablePointer(clamp(index, 0, MipmapRange::N - 1)); + dt.table2 = getTablePointer(clamp(index + 1, 0, MipmapRange::N - 1)); return dt; } // get the pair of tables for the given playback frequency (range checked) DualTable getInterpolationPairForFrequency(float freq) const { - float position = WavetableRange::getFractionalOctaveForFrequency(freq); + float position = MipmapRange::getIndexForFrequency(freq); return getInterpolationPair(position); } diff --git a/tests/DemoWavetables.cpp b/tests/DemoWavetables.cpp index e573260b..873eb68d 100644 --- a/tests/DemoWavetables.cpp +++ b/tests/DemoWavetables.cpp @@ -36,6 +36,7 @@ private: private: void valueChangedWave(int value); + void valueChangedQuality(int value); void buttonClickedPlaySweep(); private: @@ -46,6 +47,7 @@ private: sfz::WavetableOscillator fOsc; unsigned fWavePlaying = 0; std::atomic fNewWavePending { -1 }; + std::atomic fNewQualityPending { -1 }; std::atomic fStartNewSweep { false }; static constexpr float sweepMin = 0.0; @@ -88,13 +90,13 @@ bool DemoApp::initSound() fTmpFrequency.reset(new float[bufferSize]); fMulti[0] = sfz::WavetableMulti::createForHarmonicProfile( - sfz::HarmonicProfile::getSine(), 1.0, 2048); + sfz::HarmonicProfile::getSine(), sfz::config::amplitudeSine, 2048); fMulti[1] = sfz::WavetableMulti::createForHarmonicProfile( - sfz::HarmonicProfile::getTriangle(), 1.0, 2048); + sfz::HarmonicProfile::getTriangle(), sfz::config::amplitudeTriangle, 2048); fMulti[2] = sfz::WavetableMulti::createForHarmonicProfile( - sfz::HarmonicProfile::getSaw(), 1.0, 2048); + sfz::HarmonicProfile::getSaw(), sfz::config::amplitudeSaw, 2048); fMulti[3] = sfz::WavetableMulti::createForHarmonicProfile( - sfz::HarmonicProfile::getSquare(), 1.0, 2048); + sfz::HarmonicProfile::getSquare(), sfz::config::amplitudeSquare, 2048); fClient.reset(client); @@ -128,10 +130,21 @@ void DemoApp::initWindow() fUi.valWave->addItem(tr("3 - Saw")); fUi.valWave->addItem(tr("4 - Square")); + fUi.valQuality->addItem(tr("1 - Nearest")); + fUi.valQuality->addItem(tr("2 - Linear")); + fUi.valQuality->addItem(tr("3 - High")); + fUi.valQuality->addItem(tr("4 - Dual-High")); + + fUi.valQuality->setCurrentIndex(fOsc.quality()); + connect( fUi.valWave, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { valueChangedWave(index); }); + connect( + fUi.valQuality, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int index) { valueChangedQuality(index); }); + connect( fUi.btnPlaySweep, &QPushButton::clicked, this, [this]() { buttonClickedPlaySweep(); }); @@ -152,6 +165,10 @@ int DemoApp::processAudio(jack_nframes_t nframes, void* cbdata) if (newWave != -1) self->fWavePlaying = newWave; + int newQuality = self->fNewQualityPending.exchange(-1); + if (newQuality != -1) + osc.setQuality(newQuality); + osc.setWavetable(&self->fMulti[self->fWavePlaying]); float* left = reinterpret_cast( @@ -183,6 +200,11 @@ void DemoApp::valueChangedWave(int value) fNewWavePending.store(value); } +void DemoApp::valueChangedQuality(int value) +{ + fNewQualityPending.store(value); +} + void DemoApp::buttonClickedPlaySweep() { fStartNewSweep.store(true); diff --git a/tests/DemoWavetables.ui b/tests/DemoWavetables.ui index 6a5f5def..c6347b1c 100644 --- a/tests/DemoWavetables.ui +++ b/tests/DemoWavetables.ui @@ -6,7 +6,7 @@ 0 0 - 170 + 255 103 @@ -20,6 +20,13 @@ + + + Select quality + + + + Play sweep @@ -30,6 +37,9 @@ + + + @@ -38,7 +48,8 @@ - + + .. From a3943abf79ae8acf7be3468a9dcd760ca1313c26 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 8 Aug 2020 23:49:36 +0200 Subject: [PATCH 040/445] Added tests --- tests/SynthT.cpp | 241 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 214 insertions(+), 27 deletions(-) diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 17f2ee50..43ce0cf8 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -8,6 +8,7 @@ #include "sfizz/SisterVoiceRing.h" #include "sfizz/SfzHelpers.h" #include "sfizz/NumericId.h" +#include #include "catch2/catch.hpp" using namespace Catch::literals; using namespace sfz::literals; @@ -626,14 +627,48 @@ TEST_CASE("[Synth] Release") { sfz::Synth synth; synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*silence key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); synth.cc(0, 64, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 0 ); - synth.cc(0, 64, 0); REQUIRE( synth.getNumActiveVoices(true) == 1 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); +} + +TEST_CASE("[Synth] Release (pedal was already down)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*silence + key=62 sample=*sine trigger=release + )"); + synth.cc(0, 64, 127); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); +} + + + +TEST_CASE("[Synth] Release samples don't play unless there is another playing region that matches") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.cc(0, 64, 127); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 0); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); } TEST_CASE("[Synth] Release key (Different sustain CC)") @@ -646,7 +681,7 @@ TEST_CASE("[Synth] Release key (Different sustain CC)") synth.noteOn(0, 62, 85); synth.cc(0, 54, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); } TEST_CASE("[Synth] Release (Different sustain CC)") @@ -654,14 +689,15 @@ TEST_CASE("[Synth] Release (Different sustain CC)") sfz::Synth synth; synth.loadSfzString(fs::current_path(), R"( sustain_cc=54 + key=62 sample=*silence key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); synth.cc(0, 54, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 0 ); - synth.cc(0, 54, 0); REQUIRE( synth.getNumActiveVoices(true) == 1 ); + synth.cc(0, 54, 0); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); } TEST_CASE("[Synth] Sustain threshold default") @@ -681,37 +717,45 @@ TEST_CASE("[Synth] Sustain threshold") sfz::Synth synth; synth.loadSfzString(fs::current_path(), R"( sustain_lo=63 + key=62 sample=*silence key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); synth.cc(0, 64, 1); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 1 ); - synth.noteOn(0, 62, 85); - synth.noteOff(0, 62, 85); REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 4 ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 5 ); synth.cc(0, 64, 64); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 5 ); } -TEST_CASE("[Synth] Release (Multiple notes)") +template +void sortAll(C& container) { - sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( - lokey=62 hikey=64 sample=*sine trigger=release - )"); - synth.noteOn(0, 62, 85); - synth.noteOn(0, 63, 78); - synth.noteOn(0, 64, 34); - synth.cc(0, 64, 127); - synth.noteOff(0, 64, 0); - synth.noteOff(0, 63, 2); - synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 0 ); - synth.cc(0, 64, 0); - REQUIRE( synth.getNumActiveVoices(true) == 3 ); + std::sort(container.begin(), container.end()); +} + +template +void sortAll(C& container, Args&... others) +{ + std::sort(container.begin(), container.end()); + sortAll(others...); +} + +const std::vector getActiveVoices(const sfz::Synth& synth) +{ + std::vector activeVoices; + for (int i = 0; i < synth.getNumVoices(); ++i) { + const auto* voice = synth.getVoiceView(i); + if (!voice->isFree()) + activeVoices.push_back(voice); + } + return activeVoices; } TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") @@ -728,12 +772,21 @@ TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") synth.noteOff(0, 63, 2); synth.noteOff(0, 62, 85); REQUIRE( synth.getNumActiveVoices(true) == 3 ); + + std::vector requiredVelocities { 34_norm, 78_norm, 85_norm}; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); } -TEST_CASE("[Synth] Release (Multiple notes, cleared the delayed voices after)") +TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices after)") { sfz::Synth synth; synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*silence lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 )"); @@ -744,8 +797,142 @@ TEST_CASE("[Synth] Release (Multiple notes, cleared the delayed voices after)") synth.noteOff(0, 64, 0); synth.noteOff(0, 63, 2); synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + + std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared the delayed voices after)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*silence + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.cc(0, 64, 127); + synth.noteOn(1, 62, 85); + synth.noteOn(1, 63, 78); + synth.noteOn(1, 64, 34); + synth.noteOff(2, 64, 0); + synth.noteOff(2, 63, 2); + synth.noteOff(2, 62, 3); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.cc(3, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + + std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*silence + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + synth.noteOff(0, 62, 0); + synth.noteOn(0, 62, 78); + synth.noteOff(0, 62, 2); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 4 ); + + std::vector requiredVelocities { 78_norm, 85_norm, 78_norm, 85_norm }; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] No release sample after the main sample stopped sounding by default") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 4096 }; + + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=TestFiles/closedhat.wav loop_mode=one_shot + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); REQUIRE( synth.getNumActiveVoices(true) == 0 ); synth.cc(0, 64, 0); - REQUIRE( synth.getNumActiveVoices(true) == 3 ); - REQUIRE( synth.getRegionView(0)->delayedReleases.empty() ); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the attack sample died") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 4096 }; + + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=TestFiles/closedhat.wav loop_mode=one_shot + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); } From dc328cb9a7d7267a3f9a74f4d02a819cfda97cd3 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 11:06:10 +0200 Subject: [PATCH 041/445] Multiple release samples can play on pedal up --- src/sfizz/Region.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index e03e50a7..ca2c15b8 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1058,15 +1058,7 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu // If we reach this part, we're storing the notes to delay their release on CC up // This is handled by the Synth object - const auto sameNoteTest = [noteNumber](const std::pair& noteAndValue) { - return noteAndValue.first == noteNumber; - }; - - auto it = absl::c_find_if(delayedReleases, sameNoteTest); - if (it == delayedReleases.end()) - delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber)); - else - it->second = velocity; + delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber)); } return false; From e14c51e8da4c9161acfcd77cc36f06dffbe82265 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 9 Aug 2020 12:40:07 +0200 Subject: [PATCH 042/445] Fix tests --- src/sfizz/Wavetables.cpp | 13 +++++++--- src/sfizz/Wavetables.h | 1 + tests/WavetablesT.cpp | 53 +++++++++++++++++----------------------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index 352f4e2d..55a39be6 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -295,10 +295,9 @@ const std::array MipmapRange::FrequencyToIndex = []() std::array table; for (unsigned i = 0; i < table.size() - 1; ++i) { - double r = i * (1.0 / (table.size() - 1)); - double f = F1 + r * (FN - F1); - double t = std::log(K * f) / LogB; - table[i] = clamp(t, 0, N - 1); + float r = i * (1.0f / (table.size() - 1)); + float f = F1 + r * (FN - F1); + table[i] = getExactIndexForFrequency(f); } // ensure the last element to be exact table[table.size() - 1] = N - 1; @@ -321,6 +320,12 @@ float MipmapRange::getIndexForFrequency(float f) frac * FrequencyToIndex[index2]; } +float MipmapRange::getExactIndexForFrequency(float f) +{ + float t = (f < F1) ? 0.0f : (std::log(K * f) / LogB); + return clamp(t, 0, N - 1); +} + const std::array MipmapRange::IndexToStartFrequency = []() { std::array table; diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index 66b56e34..b57feeb0 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -142,6 +142,7 @@ public: static constexpr float FN = 12000.0; static float getIndexForFrequency(float f); + static float getExactIndexForFrequency(float f); static MipmapRange getRangeForIndex(int o); static MipmapRange getRangeForFrequency(float f); diff --git a/tests/WavetablesT.cpp b/tests/WavetablesT.cpp index c4d5b683..f1fd72da 100644 --- a/tests/WavetablesT.cpp +++ b/tests/WavetablesT.cpp @@ -12,45 +12,38 @@ TEST_CASE("[Wavetables] Frequency ranges") { - int cur_oct = std::numeric_limits::min(); - int min_oct = std::numeric_limits::max(); - int max_oct = std::numeric_limits::min(); + int cur_index = std::numeric_limits::min(); + int min_index = std::numeric_limits::max(); + int max_index = std::numeric_limits::min(); for (int note = 0; note < 128; ++note) { double f = midiNoteFrequency(note); - int oct = sfz::WavetableRange::getOctaveForFrequency(f); + float fractionalIndex = sfz::MipmapRange::getExactIndexForFrequency(f); + int index = static_cast(fractionalIndex); - REQUIRE(oct >= 0); - REQUIRE(oct < sfz::WavetableRange::countOctaves); + REQUIRE(index >= 0); + REQUIRE(static_cast(index) < sfz::MipmapRange::N); - REQUIRE(oct >= cur_oct); - cur_oct = oct; + float lerpFractionalIndex = sfz::MipmapRange::getIndexForFrequency(f); + int lerpIndex = static_cast(lerpFractionalIndex); - min_oct = std::min(min_oct, oct); - max_oct = std::max(max_oct, oct); + // approximation should be equal or off by 1 table in worst cases + bool lerpIndexValid = (lerpIndex - index) == 0 || (lerpIndex - index) == -1; + REQUIRE(lerpIndexValid); - sfz::WavetableRange range = sfz::WavetableRange::getRangeForOctave(oct); - REQUIRE((f >= range.minFrequency || oct == 0)); - REQUIRE((f <= range.maxFrequency || oct == sfz::WavetableRange::countOctaves - 1)); + REQUIRE(index >= cur_index); + cur_index = index; + + min_index = std::min(min_index, index); + max_index = std::max(max_index, index); + + sfz::MipmapRange range = sfz::MipmapRange::getRangeForIndex(index); + REQUIRE((f >= range.minFrequency || index == 0)); + REQUIRE((f <= range.maxFrequency || index == sfz::MipmapRange::N - 1)); } // check ranges to be decently adjusted to the MIDI frequency range - REQUIRE(min_oct == 0); - REQUIRE(max_oct == sfz::WavetableRange::countOctaves - 1); -} - -TEST_CASE("[Wavetables] Octave number lookup") -{ - for (int note = 0; note < 128; ++note) { - double f = midiNoteFrequency(note); - - float ref = std::log2(f * sfz::WavetableRange::frequencyScaleFactor); - float oct = sfz::WavetableRange::getFractionalOctaveForFrequency(f); - - ref = clamp(ref, 0, sfz::WavetableRange::countOctaves - 1); - oct = clamp(oct, 0, sfz::WavetableRange::countOctaves - 1); - - REQUIRE(oct == Approx(ref).margin(0.03f)); - } + REQUIRE(min_index == 0); + REQUIRE(max_index == sfz::MipmapRange::N - 1); } From 720ecf9b00def3c3dcb4f31ebf38cbe7cafeab51 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 9 Aug 2020 12:53:44 +0200 Subject: [PATCH 043/445] Allow wavetable alias above 20 kHz --- src/sfizz/Config.h | 1 + src/sfizz/Wavetables.h | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 03f7f378..4f5dc7d5 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -95,6 +95,7 @@ namespace config { constexpr int maxEffectBuses { 256 }; // Wavetable constants; amplitude values are matched to reference static constexpr unsigned tableSize = 1024; + static constexpr double tableRefSampleRate = 44100.0 * 1.1; // +10% aliasing permissivity static constexpr double amplitudeSine = 0.625; static constexpr double amplitudeTriangle = 0.625; static constexpr double amplitudeSaw = 0.515; diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index b57feeb0..87cd72aa 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -214,7 +214,9 @@ public: // the reference sample rate is the minimum value accepted by the DSP // system (most defavorable wrt. aliasing) static WavetableMulti createForHarmonicProfile( - const HarmonicProfile& hp, double amplitude, unsigned tableSize = config::tableSize, double refSampleRate = 44100.0); + const HarmonicProfile& hp, double amplitude, + unsigned tableSize = config::tableSize, + double refSampleRate = config::tableRefSampleRate); // get a tiny silent wavetable with null content for use with oscillators static const WavetableMulti* getSilenceWavetable(); From b1dbf0112e975ca5f6771bc85aadd4d7528b7a72 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 9 Aug 2020 15:28:01 +0200 Subject: [PATCH 044/445] Ignore released voices in search of selfmask candidate --- src/sfizz/Synth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 6081ba2b..65c91993 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -877,7 +877,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } if (region->notePolyphony) { - if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { + if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn && !voice->releasedOrFree()) { notePolyphonyCounter += 1; switch (region->selfMask) { case SfzSelfMask::mask: From 3dc98e7fd2f8d6c5631dd9d5799a945255b04ce7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 16:01:58 +0200 Subject: [PATCH 045/445] parse rt_dead --- src/sfizz/Defaults.h | 1 + src/sfizz/Region.cpp | 29 +++++++++++++++++++---------- src/sfizz/Region.h | 1 + tests/RegionT.cpp | 12 ++++++++++++ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 8be2fc96..cc0ac0b3 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -143,6 +143,7 @@ namespace Default constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power }; constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power }; constexpr float rtDecay { 0.0f }; + constexpr bool rtDead { false }; constexpr Range rtDecayRange { 0.0f, 200.0f }; // Performance parameters: Filters diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index ca2c15b8..93bc8996 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -111,7 +111,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) loopMode = SfzLoopMode::loop_sustain; break; default: - DBG("Unkown loop mode:" << std::string(opcode.value)); + DBG("Unkown loop mode:" << opcode.value); } break; case hash("loop_end"): // also loopend @@ -161,7 +161,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) offMode = SfzOffMode::normal; break; default: - DBG("Unkown off mode:" << std::string(opcode.value)); + DBG("Unkown off mode:" << opcode.value); } break; case hash("polyphony"): @@ -181,7 +181,16 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) selfMask = SfzSelfMask::dontMask; break; default: - DBG("Unkown self mask value:" << std::string(opcode.value)); + DBG("Unkown self mask value:" << opcode.value); + } + break; + case hash("rt_dead"): + if (opcode.value == "on") { + rtDead = true; + } else if (opcode.value == "off") { + rtDead = false; + } else { + DBG("Unkown rt_dead value:" << opcode.value); } break; // Region logic: key mapping @@ -274,7 +283,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) velocityOverride = SfzVelocityOverride::previous; break; default: - DBG("Unknown velocity mode: " << std::string(opcode.value)); + DBG("Unknown velocity mode: " << opcode.value); } break; @@ -337,7 +346,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) trigger = SfzTrigger::release_key; break; default: - DBG("Unknown trigger mode: " << std::string(opcode.value)); + DBG("Unknown trigger mode: " << opcode.value); } break; case hash("start_locc&"): // also on_locc& @@ -468,7 +477,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) crossfadeKeyCurve = SfzCrossfadeCurve::gain; break; default: - DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + DBG("Unknown crossfade power curve: " << opcode.value); } break; case hash("xf_velcurve"): @@ -480,7 +489,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) crossfadeVelCurve = SfzCrossfadeCurve::gain; break; default: - DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + DBG("Unknown crossfade power curve: " << opcode.value); } break; case hash("xfin_locc&"): @@ -516,7 +525,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) crossfadeCCCurve = SfzCrossfadeCurve::gain; break; default: - DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + DBG("Unknown crossfade power curve: " << opcode.value); } break; case hash("rt_decay"): @@ -636,7 +645,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) filters[filterIndex].type = *ftype; else { filters[filterIndex].type = FilterType::kFilterNone; - DBG("Unknown filter type: " << std::string(opcode.value)); + DBG("Unknown filter type: " << opcode.value); } } break; @@ -745,7 +754,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) equalizers[eqNumber - 1].type = *ftype; else { equalizers[eqNumber - 1].type = EqType::kEqNone; - DBG("Unknown EQ type: " << std::string(opcode.value)); + DBG("Unknown EQ type: " << opcode.value); } } break; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 366efd1f..3a175252 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -288,6 +288,7 @@ struct Region { absl::optional notePolyphony {}; // note_polyphony unsigned polyphony { config::maxVoices }; // polyphony SfzSelfMask selfMask { Default::selfMask }; + bool rtDead { Default::rtDead }; // Region logic: key mapping Range keyRange { Default::keyRange }; //lokey, hikey and key diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index d98b6c7a..e1d692c5 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1628,6 +1628,18 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.selfMask == SfzSelfMask::dontMask); } + SECTION("Release dead") + { + REQUIRE(region.rtDead == false); + region.parseOpcode({ "rt_dead", "on" }); + REQUIRE(region.rtDead == true); + region.parseOpcode({ "rt_dead", "off" }); + REQUIRE(region.rtDead == false); + region.parseOpcode({ "rt_dead", "on" }); + region.parseOpcode({ "rt_dead", "garbage" }); + REQUIRE(region.rtDead == true); + } + SECTION("amplitude") { REQUIRE(region.amplitude == 1.0_a); From bbf22b058e9c8b61ed658a35452c1dd4ec7ae110 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 16:02:14 +0200 Subject: [PATCH 046/445] Small test cleanups --- tests/SynthT.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 43ce0cf8..a71cb91d 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -201,6 +201,7 @@ TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at t synth.setNumVoices(1); synth.loadSfzString(fs::current_path() / "tests/TestFiles/envelope_trigger_release.sfz", R"( lovel=0 hivel=127 + sample=*silence trigger=release sample=*noise loop_mode=one_shot ampeg_attack=0.02 ampeg_decay=0.02 ampeg_release=0.1 ampeg_sustain=0 )"); @@ -681,7 +682,7 @@ TEST_CASE("[Synth] Release key (Different sustain CC)") synth.noteOn(0, 62, 85); synth.cc(0, 54, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); } TEST_CASE("[Synth] Release (Different sustain CC)") @@ -872,10 +873,11 @@ TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") TEST_CASE("[Synth] No release sample after the main sample stopped sounding by default") { sfz::Synth synth; + synth.setSamplesPerBlock(4096); sfz::AudioBuffer buffer { 2, 4096 }; synth.loadSfzString(fs::current_path(), R"( - lokey=62 hikey=64 sample=TestFiles/closedhat.wav loop_mode=one_shot + lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 )"); @@ -906,10 +908,11 @@ TEST_CASE("[Synth] No release sample after the main sample stopped sounding by d TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the attack sample died") { sfz::Synth synth; + synth.setSamplesPerBlock(4096); sfz::AudioBuffer buffer { 2, 4096 }; synth.loadSfzString(fs::current_path(), R"( - lokey=62 hikey=64 sample=TestFiles/closedhat.wav loop_mode=one_shot + lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 )"); From 39abff2ccacbee9343f1a5ee9da539571470831c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 16:02:41 +0200 Subject: [PATCH 047/445] Check that there is a voice playing for release samples --- src/sfizz/Synth.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 0ab5664b..31928c2c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -844,6 +844,25 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { + if (region->triggerOnNote && region->trigger == SfzTrigger::release && !region->rtDead) { + // check that a voice with compatible trigger is playing + // FIXME: we're going twice over the voices, when the synth + // handles the regions completely these dispatch functions + // should be overhauled, also to include voice stealing on + // all events + const auto compatibleVoice = [region](const VoicePtr& v) -> bool { + return ( + !v->isFree() + && v->getTriggerType() == Voice::TriggerType::NoteOn + && region->keyRange.containsWithEnd(v->getTriggerNumber()) + && region->velocityRange.containsWithEnd(v->getTriggerValue()) + ); + }; + + if (absl::c_find_if(voices, compatibleVoice) == voices.end()) + continue; + } + auto voice = findFreeVoice(); if (voice == nullptr) continue; @@ -1002,6 +1021,27 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept for (auto& region : ccActivationLists[ccNumber]) { if (ccNumber == region->sustainCC) { + if (!region->rtDead) { + // check that a voice with compatible trigger is playing + // FIXME: we're going twice over the voices, when the synth + // handles the regions completely these dispatch functions + // should be overhauled, also to include voice stealing on + // all events + const auto compatibleVoice = [region](const VoicePtr& v) -> bool { + return ( + !v->isFree() + && v->getTriggerType() == Voice::TriggerType::NoteOn + && region->keyRange.containsWithEnd(v->getTriggerNumber()) + && region->velocityRange.containsWithEnd(v->getTriggerValue()) + ); + }; + + if (absl::c_find_if(voices, compatibleVoice) == voices.end()) { + region->delayedReleases.clear(); + continue; + } + } + for (auto& note: region->delayedReleases) { // FIXME: we really need to have some form of common method to find and start voices... auto voice = findFreeVoice(); From cb367d385fbb8ad79870eabffae4fcfb5b905684 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 16:41:47 +0200 Subject: [PATCH 048/445] Make the matching function a free one --- src/sfizz/Synth.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 31928c2c..c2f6114f 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -837,6 +837,15 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept noteOffDispatch(delay, noteNumber, replacedVelocity); } +bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) { + return ( + !voice.isFree() + && voice.getTriggerType() == sfz::Voice::TriggerType::NoteOn + && region.keyRange.containsWithEnd(voice.getTriggerNumber()) + && region.velocityRange.containsWithEnd(voice.getTriggerValue()) + ); +} + void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); @@ -851,12 +860,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex // should be overhauled, also to include voice stealing on // all events const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return ( - !v->isFree() - && v->getTriggerType() == Voice::TriggerType::NoteOn - && region->keyRange.containsWithEnd(v->getTriggerNumber()) - && region->velocityRange.containsWithEnd(v->getTriggerValue()) - ); + return matchReleaseRegionAndVoice(*region, *v); }; if (absl::c_find_if(voices, compatibleVoice) == voices.end()) @@ -1028,12 +1032,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept // should be overhauled, also to include voice stealing on // all events const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return ( - !v->isFree() - && v->getTriggerType() == Voice::TriggerType::NoteOn - && region->keyRange.containsWithEnd(v->getTriggerNumber()) - && region->velocityRange.containsWithEnd(v->getTriggerValue()) - ); + return matchReleaseRegionAndVoice(*region, *v); }; if (absl::c_find_if(voices, compatibleVoice) == voices.end()) { From fa646b1e73ba612bb56a4c1ea67e1a20431ca220 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 20:14:16 +0200 Subject: [PATCH 049/445] Change the check order --- src/sfizz/Synth.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 65c91993..47fc507d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -877,7 +877,9 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } if (region->notePolyphony) { - if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn && !voice->releasedOrFree()) { + if (!voice->releasedOrFree() + && voice->getTriggerNumber() == noteNumber + && voice->getTriggerType() == Voice::TriggerType::NoteOn) { notePolyphonyCounter += 1; switch (region->selfMask) { case SfzSelfMask::mask: From 675d7f3fc5ea234e78aa27bb65bdfbb4cba32ac6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 19:25:59 +0200 Subject: [PATCH 050/445] Add modulation matrix --- dpf.mk | 5 + src/CMakeLists.txt | 11 + src/sfizz/Region.cpp | 64 +++- src/sfizz/Region.h | 8 +- src/sfizz/Resources.h | 5 + src/sfizz/Synth.cpp | 61 ++++ src/sfizz/Synth.h | 10 + src/sfizz/Voice.cpp | 97 +++--- src/sfizz/modulations/ModGenerator.h | 52 +++ src/sfizz/modulations/ModId.cpp | 54 ++++ src/sfizz/modulations/ModId.h | 87 +++++ src/sfizz/modulations/ModKey.cpp | 93 ++++++ src/sfizz/modulations/ModKey.h | 68 ++++ src/sfizz/modulations/ModKeyHash.cpp | 34 ++ src/sfizz/modulations/ModKeyHash.h | 17 + src/sfizz/modulations/ModMatrix.cpp | 319 +++++++++++++++++++ src/sfizz/modulations/ModMatrix.h | 160 ++++++++++ src/sfizz/modulations/sources/Controller.cpp | 97 ++++++ src/sfizz/modulations/sources/Controller.h | 29 ++ tests/CMakeLists.txt | 1 + tests/ModulationsT.cpp | 76 +++++ 21 files changed, 1291 insertions(+), 57 deletions(-) create mode 100644 src/sfizz/modulations/ModGenerator.h create mode 100644 src/sfizz/modulations/ModId.cpp create mode 100644 src/sfizz/modulations/ModId.h create mode 100644 src/sfizz/modulations/ModKey.cpp create mode 100644 src/sfizz/modulations/ModKey.h create mode 100644 src/sfizz/modulations/ModKeyHash.cpp create mode 100644 src/sfizz/modulations/ModKeyHash.h create mode 100644 src/sfizz/modulations/ModMatrix.cpp create mode 100644 src/sfizz/modulations/ModMatrix.h create mode 100644 src/sfizz/modulations/sources/Controller.cpp create mode 100644 src/sfizz/modulations/sources/Controller.h create mode 100644 tests/ModulationsT.cpp diff --git a/dpf.mk b/dpf.mk index f4442beb..4060db8d 100644 --- a/dpf.mk +++ b/dpf.mk @@ -60,6 +60,11 @@ SFIZZ_SOURCES = \ src/sfizz/Curve.cpp \ src/sfizz/effects/Apan.cpp \ src/sfizz/Effects.cpp \ + src/sfizz/modulations/ModId.cpp \ + src/sfizz/modulations/ModKey.cpp \ + src/sfizz/modulations/ModKeyHash.cpp \ + src/sfizz/modulations/ModMatrix.cpp \ + src/sfizz/modulations/sources/Controller.cpp \ src/sfizz/effects/Compressor.cpp \ src/sfizz/effects/Disto.cpp \ src/sfizz/effects/Eq.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c694239..f0343c5e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,12 @@ set (SFIZZ_HEADERS sfizz/Debug.h sfizz/utility/SpinMutex.h sfizz/utility/SpinMutex.cpp + sfizz/modulations/ModId.h + sfizz/modulations/ModKey.h + sfizz/modulations/ModKeyHash.h + sfizz/modulations/ModMatrix.h + sfizz/modulations/ModGenerator.h + sfizz/modulations/sources/Controller.h sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArraySSE.h @@ -128,6 +134,11 @@ set (SFIZZ_SOURCES sfizz/RTSemaphore.cpp sfizz/Panning.cpp sfizz/Effects.cpp + sfizz/modulations/ModId.cpp + sfizz/modulations/ModKey.cpp + sfizz/modulations/ModKeyHash.cpp + sfizz/modulations/ModMatrix.cpp + sfizz/modulations/sources/Controller.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 0a19625c..c8775c0b 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -11,6 +11,7 @@ #include "Opcode.h" #include "StringViewHelpers.h" #include "ModifierHelpers.h" +#include "modulations/ModId.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_cat.h" #include "absl/algorithm/container.h" @@ -378,35 +379,35 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, volume, Default::volumeRange); break; case_any_ccN("volume"): // also gain - processGenericCc(opcode, Default::volumeCCRange, &modifiers[Mod::volume]); + processGenericCc(opcode, Default::volumeCCRange, &modifiers[Mod::volume], ModKey::createNXYZ(ModId::Volume, id)); break; case hash("amplitude"): if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) amplitude = normalizePercents(*value); break; case_any_ccN("amplitude"): - processGenericCc(opcode, Default::amplitudeRange, &modifiers[Mod::amplitude]); + processGenericCc(opcode, Default::amplitudeRange, &modifiers[Mod::amplitude], ModKey::createNXYZ(ModId::Amplitude, id)); break; case hash("pan"): if (auto value = readOpcode(opcode.value, Default::panRange)) pan = normalizePercents(*value); break; case_any_ccN("pan"): - processGenericCc(opcode, Default::panCCRange, &modifiers[Mod::pan]); + processGenericCc(opcode, Default::panCCRange, &modifiers[Mod::pan], ModKey::createNXYZ(ModId::Pan, id)); break; case hash("position"): if (auto value = readOpcode(opcode.value, Default::positionRange)) position = normalizePercents(*value); break; case_any_ccN("position"): - processGenericCc(opcode, Default::positionCCRange, &modifiers[Mod::position]); + processGenericCc(opcode, Default::positionCCRange, &modifiers[Mod::position], ModKey::createNXYZ(ModId::Position, id)); break; case hash("width"): if (auto value = readOpcode(opcode.value, Default::widthRange)) width = normalizePercents(*value); break; case_any_ccN("width"): - processGenericCc(opcode, Default::widthCCRange, &modifiers[Mod::width]); + processGenericCc(opcode, Default::widthCCRange, &modifiers[Mod::width], ModKey::createNXYZ(ModId::Width, id)); break; case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); @@ -770,7 +771,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, tune, Default::tuneRange); break; case_any_ccN("pitch"): // also tune - processGenericCc(opcode, Default::tuneCCRange, &modifiers[Mod::pitch]); + processGenericCc(opcode, Default::tuneCCRange, &modifiers[Mod::pitch], ModKey::createNXYZ(ModId::Pitch, id)); break; case hash("bend_up"): // also bendup setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); @@ -924,7 +925,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return true; } -bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap) +bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap, const ModKey& target) { if (!opcode.isAnyCcN()) return false; @@ -933,6 +934,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCM if (ccNumber >= config::numCCs) return false; + // TODO obsolete after implementing mod matrix if (ccMap) { Modifier& modifier = (*ccMap)[ccNumber]; switch (opcode.category) { @@ -955,7 +957,53 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCM assert(false); break; } - } + } + + if (target) { + // search an existing connection of same CC number and target + // if it exists, modify, otherwise create + auto it = std::find_if(connections.begin(), connections.end(), + [ccNumber, &target](const Connection& x) -> bool + { + return x.first.id() == ModId::Controller && + x.first.parameters().cc == ccNumber && + x.second == target; + }); + + Connection *conn; + if (it != connections.end()) + conn = &*it; + else { + connections.emplace_back(); + conn = &connections.back(); + conn->first = ModKey::createCC(ccNumber, 0, 0, 0, 0); + conn->second = target; + } + + // + ModKey::Parameters p = conn->first.parameters(); + switch (opcode.category) { + case kOpcodeOnCcN: + setValueFromOpcode(opcode, p.value, range); + break; + case kOpcodeCurveCcN: + setValueFromOpcode(opcode, p.curve, Default::curveCCRange); + break; + case kOpcodeStepCcN: + { + const Range stepCCRange { 0.0f, std::max(std::abs(range.getStart()), std::abs(range.getEnd())) }; + setValueFromOpcode(opcode, p.step, stepCCRange); + } + break; + case kOpcodeSmoothCcN: + setValueFromOpcode(opcode, p.smooth, Default::smoothCCRange); + break; + default: + assert(false); + break; + } + conn->first = ModKey(ModId::Controller, {}, p); + } return true; } diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 772ee399..da044fd9 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -18,6 +18,7 @@ #include "FileId.h" #include "NumericId.h" #include "Modifiers.h" +#include "modulations/ModKey.h" #include "absl/types/optional.h" #include #include @@ -241,10 +242,11 @@ struct Region { * @param opcode * @param range * @param ccMap + * @param target * @return true if the opcode was properly read and stored. * @return false */ - bool processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap); + bool processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap, const ModKey& target); void offsetAllKeys(int offset) noexcept; @@ -374,6 +376,10 @@ struct Region { bool triggerOnCC { false }; // whether the region triggers on CC events or note events bool triggerOnNote { true }; + // Modulation matrix connections + typedef std::pair Connection; + std::vector connections; + // Parent RegionSet* parent { nullptr }; private: diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 0c28427e..645482cd 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -14,6 +14,7 @@ #include "Wavetables.h" #include "Curve.h" #include "Tuning.h" +#include "modulations/ModMatrix.h" #include "absl/types/optional.h" namespace sfz @@ -33,18 +34,21 @@ struct Resources WavetablePool wavePool; Tuning tuning; absl::optional stretch; + ModMatrix modMatrix; void setSampleRate(float samplerate) { midiState.setSampleRate(samplerate); filterPool.setSampleRate(samplerate); eqPool.setSampleRate(samplerate); + modMatrix.setSampleRate(samplerate); } void setSamplesPerBlock(int samplesPerBlock) { bufferPool.setBufferSize(samplesPerBlock); midiState.setSamplesPerBlock(samplesPerBlock); + modMatrix.setSamplesPerBlock(samplesPerBlock); } void clear() @@ -54,6 +58,7 @@ struct Resources wavePool.clearFileWaves(); logger.clear(); midiState.reset(); + modMatrix.clear(); } }; } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 47fc507d..7bae9797 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -12,6 +12,10 @@ #include "ModifierHelpers.h" #include "ScopedFTZ.h" #include "StringViewHelpers.h" +#include "modulations/ModMatrix.h" +#include "modulations/ModKey.h" +#include "modulations/ModId.h" +#include "modulations/sources/Controller.h" #include "pugixml.hpp" #include "absl/algorithm/container.h" #include "absl/memory/memory.h" @@ -35,6 +39,9 @@ sfz::Synth::Synth(int numVoices) effectFactory.registerStandardEffectTypes(); effectBuses.reserve(5); // sufficient room for main and fx1-4 resetVoices(numVoices); + + // modulation sources + genController.reset(new ControllerSource(resources)); } sfz::Synth::~Synth() @@ -570,6 +577,8 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxModifiers = maxModifiers; applySettingsPerVoice(); + + setupModMatrix(); } bool sfz::Synth::loadScalaFile(const fs::path& path) @@ -717,6 +726,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept return; } + ModMatrix& mm = resources.modMatrix; + activeVoices = 0; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; @@ -724,6 +735,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept tempMixSpan->fill(0.0f); resources.filePool.cleanupPromises(); + mm.beginCycle(numFrames); + // Ramp out whatever is in the buffer at this point; should only be killed voice data linearRamp(*rampSpan, 1.0f, -1.0f / static_cast(numFrames)); for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { @@ -736,6 +749,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; + mm.beginVoice(voice->getId()); + activeVoices++; renderVoiceToOutputs(*voice, *tempSpan); callbackBreakdown.data += voice->getLastDataDuration(); @@ -1335,6 +1350,52 @@ void sfz::Synth::applySettingsPerVoice() } } +void sfz::Synth::setupModMatrix() +{ + ModMatrix& mm = resources.modMatrix; + + for (const RegionPtr& region : regions) { + for (const Region::Connection& conn : region->connections) { + ModGenerator* gen = nullptr; + + switch (conn.first.id()) { + case ModId::Controller: + gen = genController.get(); + break; + default: + DBG("[sfizz] Have unknown type of source generator"); + break; + } + + ASSERT(gen); + if (!gen) + continue; + + ModMatrix::SourceId source = mm.registerSource(conn.first, *gen); + ModMatrix::TargetId target = mm.registerTarget(conn.second); + + ASSERT(source); + if (!source) { + DBG("[sfizz] Failed to register modulation source"); + continue; + } + + ASSERT(target); + if (!source) { + DBG("[sfizz] Failed to register modulation target"); + continue; + } + + if (!mm.connect(source, target)) { + DBG("[sfizz] Failed to connect modulation source and target"); + ASSERTFALSE; + } + } + } + + mm.init(); +} + void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept { const std::lock_guard disableCallback { callbackGuard }; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 13b1f9ba..5c41078d 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -26,6 +26,8 @@ #include namespace sfz { +class ControllerSource; + /** * @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 @@ -677,6 +679,11 @@ private: */ void applySettingsPerVoice(); + /** + * @brief Establish all connections of the modulation matrix. + */ + void setupModMatrix(); + /** * @brief Render the voice to its designated outputs and effect busses. * @@ -758,6 +765,9 @@ private: int noteOffset { 0 }; int octaveOffset { 0 }; + // Modulation source generators + std::unique_ptr genController; + // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 910fed95..a9ced8b5 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -12,6 +12,9 @@ #include "SIMDHelpers.h" #include "Panning.h" #include "SfzHelpers.h" +#include "modulations/ModId.h" +#include "modulations/ModKey.h" +#include "modulations/ModMatrix.h" #include "Interpolators.h" #include "absl/algorithm/container.h" @@ -164,6 +167,8 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, smoother.setSmoothing(mod.data.smooth, sampleRate); }); } + + resources.modMatrix.initVoice(id); } int sfz::Voice::getCurrentSampleQuality() const noexcept @@ -374,30 +379,24 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept { const auto numSamples = modulationSpan.size(); - auto tempSpan = resources.bufferPool.getBuffer(numSamples); - if (!tempSpan) - return; + ModMatrix& mm = resources.modMatrix; + const ModKey volumeKey = ModKey::createNXYZ(ModId::Volume, region->getId()); + const ModKey amplitudeKey = ModKey::createNXYZ(ModId::Amplitude, region->getId()); // AmpEG envelope egEnvelope.getBlock(modulationSpan); // Amplitude envelope - applyGain1(baseGain, modulationSpan); - forEachWithSmoother(Mod::amplitude, [&](const CCData& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - applyGain(*tempSpan, modulationSpan); - }); + if (float* mod = mm.getModulationByKey(amplitudeKey)) { + for (size_t i = 0; i < numSamples; ++i) + modulationSpan[i] *= normalizePercents(mod[i]); + } // Volume envelope - applyGain1(db2mag(baseVolumedB), modulationSpan); - forEachWithSmoother(Mod::volume, [&](const CCData& mod, Smoother& smoother) { - multiplicativeModifier(resources, *tempSpan, mod, [](float x) { - return db2mag(x); - }); - smoother.process(*tempSpan, *tempSpan); - applyGain(*tempSpan, modulationSpan); - }); + if (float* mod = mm.getModulationByKey(volumeKey)) { + for (size_t i = 0; i < numSamples; ++i) + modulationSpan[i] *= db2mag(mod[i]); + } // Smooth the gain transitions gainSmoother.process(modulationSpan, modulationSpan); @@ -442,20 +441,21 @@ void sfz::Voice::panStageMono(AudioSpan buffer) noexcept const auto rightBuffer = buffer.getSpan(1); auto modulationSpan = resources.bufferPool.getBuffer(numSamples); - auto tempSpan = resources.bufferPool.getBuffer(numSamples); - if (!modulationSpan || !tempSpan) + if (!modulationSpan) return; + ModMatrix& mm = resources.modMatrix; + const ModKey panKey = ModKey::createNXYZ(ModId::Pan, region->getId()); + // Prepare for stereo output copy(leftBuffer, rightBuffer); // Apply panning fill(*modulationSpan, region->pan); - forEachWithSmoother(Mod::pan, [&](const CCData& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*tempSpan, *modulationSpan); - }); + if (float* mod = mm.getModulationByKey(panKey)) { + for (size_t i = 0; i < numSamples; ++i) + (*modulationSpan)[i] += normalizePercents(mod[i]); + } pan(*modulationSpan, leftBuffer, rightBuffer); } @@ -467,34 +467,35 @@ void sfz::Voice::panStageStereo(AudioSpan buffer) noexcept const auto rightBuffer = buffer.getSpan(1); auto modulationSpan = resources.bufferPool.getBuffer(numSamples); - auto tempSpan = resources.bufferPool.getBuffer(numSamples); - if (!modulationSpan || !tempSpan) + if (!modulationSpan) return; + ModMatrix& mm = resources.modMatrix; + const ModKey panKey = ModKey::createNXYZ(ModId::Pan, region->getId()); + const ModKey widthKey = ModKey::createNXYZ(ModId::Width, region->getId()); + const ModKey positionKey = ModKey::createNXYZ(ModId::Position, region->getId()); + // Apply panning fill(*modulationSpan, region->pan); - forEachWithSmoother(Mod::pan, [&](const CCData& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*tempSpan, *modulationSpan); - }); + if (float* mod = mm.getModulationByKey(panKey)) { + for (size_t i = 0; i < numSamples; ++i) + (*modulationSpan)[i] += normalizePercents(mod[i]); + } pan(*modulationSpan, leftBuffer, rightBuffer); // Apply the width/position process fill(*modulationSpan, region->width); - forEachWithSmoother(Mod::width, [&](const CCData& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*tempSpan, *modulationSpan); - }); + if (float* mod = mm.getModulationByKey(widthKey)) { + for (size_t i = 0; i < numSamples; ++i) + (*modulationSpan)[i] += normalizePercents(mod[i]); + } width(*modulationSpan, leftBuffer, rightBuffer); fill(*modulationSpan, region->position); - forEachWithSmoother(Mod::position, [&](const CCData& mod, Smoother& smoother) { - linearModifier(resources, *tempSpan, mod, normalizePercents); - smoother.process(*tempSpan, *tempSpan); - add(*tempSpan, *modulationSpan); - }); + if (float* mod = mm.getModulationByKey(positionKey)) { + for (size_t i = 0; i < numSamples; ++i) + (*modulationSpan)[i] += normalizePercents(mod[i]); + } pan(*modulationSpan, leftBuffer, rightBuffer); } @@ -906,13 +907,13 @@ void sfz::Voice::pitchEnvelope(absl::Span pitchSpan) noexcept bendSmoother.process(*bends, *bends); applyGain(*bends, pitchSpan); - forEachWithSmoother(Mod::pitch, [&](const CCData& mod, Smoother& smoother) { - multiplicativeModifier(resources, *bends, mod, [](float x) { - return centsFactor(x); - }); - smoother.process(*bends, *bends); - applyGain(*bends, pitchSpan); - }); + ModMatrix& mm = resources.modMatrix; + const ModKey pitchKey = ModKey::createNXYZ(ModId::Pitch, region->getId()); + + if (float* mod = mm.getModulationByKey(pitchKey)) { + for (size_t i = 0; i < numFrames; ++i) + pitchSpan[i] *= centsFactor(mod[i]); + } } void sfz::Voice::resetSmoothers() noexcept diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h new file mode 100644 index 00000000..c146593d --- /dev/null +++ b/src/sfizz/modulations/ModGenerator.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../NumericId.h" +#include +#include + +namespace sfz { + +class ModKey; +class Voice; + +/** + * @brief Generator for modulation sources + */ +class ModGenerator { +public: + virtual ~ModGenerator() {} + + /** + * @brief Set the sample rate + */ + virtual void setSampleRate(double sampleRate) = 0; + + /** + * @brief Set the maximum block size + */ + virtual void setSamplesPerBlock(unsigned count) = 0; + + /** + * @brief Initialize the generator. + * + * @param sourceKey identifier of the source to initialize + * @param voiceId the particular voice to initialize, if per-voice + */ + virtual void init(const ModKey& sourceKey, NumericId voiceId) = 0; + + /** + * @brief Generate a cycle of the modulator + * + * @param sourceKey source key + * @param voiceNum voice number if the generator is per-voice, otherwise undefined + * @param buffer output buffer + */ + virtual void generate(const ModKey& sourceKey, NumericId voiceNum, absl::Span buffer) = 0; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp new file mode 100644 index 00000000..a75c648c --- /dev/null +++ b/src/sfizz/modulations/ModId.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ModId.h" + +namespace sfz { + +bool ModIds::isSource(ModId id) noexcept +{ + return static_cast(id) >= static_cast(ModId::_SourcesStart) && + static_cast(id) < static_cast(ModId::_SourcesEnd); +} + +bool ModIds::isTarget(ModId id) noexcept +{ + return static_cast(id) >= static_cast(ModId::_TargetsStart) && + static_cast(id) < static_cast(ModId::_TargetsEnd); +} + +int ModIds::flags(ModId id) noexcept +{ + switch (id) { + // sources + case ModId::Controller: + return kModIsPerCycle; + case ModId::Envelope: + return kModIsPerVoice; + case ModId::LFO: + return kModIsPerVoice; + + // targets + case ModId::Amplitude: + return kModIsPerVoice|kModIsPercentMultiplicative; + case ModId::Pan: + return kModIsPerVoice|kModIsAdditive; + case ModId::Width: + return kModIsPerVoice|kModIsAdditive; + case ModId::Position: + return kModIsPerVoice|kModIsAdditive; + case ModId::Pitch: + return kModIsPerVoice|kModIsAdditive; + case ModId::Volume: + return kModIsPerVoice|kModIsAdditive; + + // unknown + default: + return kModFlagsInvalid; + } +} + +} // namespace sfz diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h new file mode 100644 index 00000000..8f18679a --- /dev/null +++ b/src/sfizz/modulations/ModId.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once + +namespace sfz { + +/** + * @brief Generic identifier of a kind of modulation source or target, + * not necessarily unique per SFZ instrument + */ +enum class ModId : int { + Undefined, + + //-------------------------------------------------------------------------- + // Sources + //-------------------------------------------------------------------------- + _SourcesStart, + + Controller = _SourcesStart, + Envelope, + LFO, + + _SourcesEnd, + + //-------------------------------------------------------------------------- + // Targets + //-------------------------------------------------------------------------- + _TargetsStart = _SourcesEnd, + + Amplitude = _TargetsStart, + Pan, + Width, + Position, + Pitch, + Volume, + + _TargetsEnd, + // [/targets] -------------------------------------------------------------- +}; + +/** + * @brief Modulation bit flags (S=source, T=target, ST=either) + */ +enum ModFlags : int { + //! This modulation is invalid. (ST) + kModFlagsInvalid = -1, + + //! This modulation is global (the default). (ST) + kModIsPerCycle = 1 << 1, + //! This modulation is updated separately for every region of every voice (ST) + kModIsPerVoice = 1 << 2, + + //! This target is additive. (T) + kModIsAdditive = 1 << 3, + //! This target is multiplicative (T) + kModIsMultiplicative = 1 << 4, + //! This target is %-multiplicative (T) + kModIsPercentMultiplicative = 1 << 5, +}; + +namespace ModIds { + +bool isSource(ModId id) noexcept; +bool isTarget(ModId id) noexcept; +int flags(ModId id) noexcept; + +template inline void forEachSourceId(F&& f) +{ + for (int i = static_cast(ModId::_SourcesStart); + i < static_cast(ModId::_SourcesEnd); ++i) + f(static_cast(i)); +} + +template inline void forEachTargetId(F&& f) +{ + for (int i = static_cast(ModId::_TargetsStart); + i < static_cast(ModId::_TargetsEnd); ++i) + f(static_cast(i)); +} + +} // namespace ModIds + +} // namespace sfz diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp new file mode 100644 index 00000000..3a865bd7 --- /dev/null +++ b/src/sfizz/modulations/ModKey.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ModKey.h" +#include "ModId.h" +#include "../Debug.h" +#include +#include + +namespace sfz { + +ModKey ModKey::createCC(uint8_t cc, uint8_t curve, uint8_t smooth, float value, float step) +{ + ModKey::Parameters p; + p.cc = cc; + p.curve = curve; + p.smooth = smooth; + p.value = value; + p.step = step; + return ModKey(ModId::Controller, {}, p); +} + +ModKey ModKey::createNXYZ(ModId id, NumericId region, uint8_t N, uint8_t X, uint8_t Y, uint8_t Z) +{ + ASSERT(id != ModId::Controller); + ModKey::Parameters p; + p.N = N; + p.X = X; + p.Y = Y; + p.Z = Z; + return ModKey(id, region, p); +} + +bool ModKey::isSource() const noexcept +{ + return ModIds::isSource(id_); +} + +bool ModKey::isTarget() const noexcept +{ + return ModIds::isTarget(id_); +} + +int ModKey::flags() const noexcept +{ + return ModIds::flags(id_); +} + +std::string ModKey::toString() const +{ + switch (id_) { + case ModId::Controller: + return absl::StrCat("Controller ", params_.cc, + " {curve=", params_.curve, ", smooth=", params_.smooth, + ", value=", params_.value, ", step=", params_.value, "}"); + case ModId::Envelope: + return absl::StrCat("EG ", 1 + params_.N); + case ModId::LFO: + return absl::StrCat("LFO ", 1 + params_.N); + + case ModId::Amplitude: + return "Amplitude"; + case ModId::Pan: + return "Pan"; + case ModId::Width: + return "Width"; + case ModId::Position: + return "Position"; + case ModId::Pitch: + return "Pitch"; + case ModId::Volume: + return "Volume"; + + default: + return {}; + } +} + +} // namespace sfz + +bool sfz::ModKey::operator==(const ModKey &other) const noexcept +{ + return id_ == other.id_ && region_ && other.region_ && + !std::memcmp(¶meters(), &other.parameters(), sizeof(ModKey::Parameters)); +} + +bool sfz::ModKey::operator!=(const ModKey &other) const noexcept +{ + return !this->operator==(other); +} diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h new file mode 100644 index 00000000..e70c244d --- /dev/null +++ b/src/sfizz/modulations/ModKey.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "ModKeyHash.h" +#include "../NumericId.h" +#include +#include + +namespace sfz { + +struct Region; + +enum class ModId : int; + +/** + * @brief Identifier of a single modulation source or target within a SFZ instrument + */ +class ModKey { +public: + struct Parameters; + + ModKey() = default; + explicit ModKey(ModId id, NumericId region = {}, Parameters params = {}) + : id_(id), region_(region), params_(params) {} + + static ModKey createCC(uint8_t cc, uint8_t curve, uint8_t smooth, float value, float step); + static ModKey createNXYZ(ModId id, NumericId region, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0); + + explicit operator bool() const noexcept { return id_ != ModId(); } + + const ModId& id() const noexcept { return id_; } + NumericId region() const noexcept { return region_; } + const Parameters& parameters() const noexcept { return params_; } + + bool isSource() const noexcept; + bool isTarget() const noexcept; + int flags() const noexcept; + std::string toString() const; + + struct Parameters { + Parameters() { std::memset(this, 0, sizeof(*this)); } + union { + //! Parameters if this key identifies a CC source + struct { uint8_t cc, curve, smooth; float value, step; }; + //! Parameters otherwise, based on the related opcode + // eg. `N` in `lfoN`, `N, X` in `lfoN_eqX` + struct { uint8_t N, X, Y, Z; }; + }; + }; + +public: + bool operator==(const ModKey &other) const noexcept; + bool operator!=(const ModKey &other) const noexcept; + +private: + //! Identifier + ModId id_ {}; + //! Region identifier, only applicable if the modulation is per-voice + NumericId region_; + //! List of values which identify the key uniquely, along with the hash and region + Parameters params_ {}; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/ModKeyHash.cpp b/src/sfizz/modulations/ModKeyHash.cpp new file mode 100644 index 00000000..8e274a45 --- /dev/null +++ b/src/sfizz/modulations/ModKeyHash.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ModKeyHash.h" +#include "ModKey.h" +#include "ModId.h" +#include "StringViewHelpers.h" +#include + +size_t std::hash::operator()(const sfz::ModKey &key) const +{ + uint64_t k = hashNumber(static_cast(key.id())); + const sfz::ModKey::Parameters& p = key.parameters(); + + switch (key.id()) { + case sfz::ModId::Controller: + k = hashNumber(p.cc, k); + k = hashNumber(p.curve, k); + k = hashNumber(p.smooth, k); + k = hashNumber(p.value, k); + k = hashNumber(p.step, k); + break; + default: + k = hashNumber(p.N, k); + k = hashNumber(p.X, k); + k = hashNumber(p.Y, k); + k = hashNumber(p.Z, k); + break; + } + return k; +} diff --git a/src/sfizz/modulations/ModKeyHash.h b/src/sfizz/modulations/ModKeyHash.h new file mode 100644 index 00000000..a5cdf4c6 --- /dev/null +++ b/src/sfizz/modulations/ModKeyHash.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include + +namespace sfz { class ModKey; } + +namespace std { + template struct hash; + template <> struct hash { + size_t operator()(const sfz::ModKey &key) const; + }; +} diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp new file mode 100644 index 00000000..dd1bd4c3 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ModMatrix.h" +#include "ModId.h" +#include "ModKey.h" +#include "ModGenerator.h" +#include "Buffer.h" +#include "Config.h" +#include "SIMDHelpers.h" +#include "Debug.h" +#include +#include +#include + +namespace sfz { + +struct ModMatrix::Impl { + double sampleRate_ {}; + uint32_t samplesPerBlock_ {}; + + uint32_t numFrames_ {}; + NumericId voiceId_ {}; + + struct Source { + ModKey key; + ModGenerator* gen {}; + bool bufferReady {}; + Buffer buffer; + }; + + struct ConnectionData { + // nothing + }; + + struct Target { + ModKey key; + uint32_t region {}; + absl::flat_hash_map connectedSources; + bool bufferReady {}; + Buffer buffer; + }; + + absl::flat_hash_map sourceIndex_; + absl::flat_hash_map targetIndex_; + + std::vector sources_; + std::vector targets_; + + Buffer temp_; +}; + +ModMatrix::ModMatrix() + : impl_(new Impl) +{ + setSampleRate(config::defaultSampleRate); + setSamplesPerBlock(config::defaultSamplesPerBlock); +} + +ModMatrix::~ModMatrix() +{ +} + +void ModMatrix::clear() +{ + Impl& impl = *impl_; + + impl.sourceIndex_.clear(); + impl.targetIndex_.clear(); + impl.sources_.clear(); + impl.targets_.clear(); +} + +void ModMatrix::setSampleRate(double sampleRate) +{ + Impl& impl = *impl_; + + if (impl.sampleRate_ == sampleRate) + return; + + impl.sampleRate_ = sampleRate; + + for (Impl::Source &source : impl.sources_) + source.gen->setSampleRate(sampleRate); +} + +void ModMatrix::setSamplesPerBlock(unsigned samplesPerBlock) +{ + Impl& impl = *impl_; + + if (impl.samplesPerBlock_ == samplesPerBlock) + return; + + impl.samplesPerBlock_ = samplesPerBlock; + + for (Impl::Source &source : impl.sources_) { + source.buffer.resize(samplesPerBlock); + source.gen->setSamplesPerBlock(samplesPerBlock); + } + for (Impl::Target &target : impl.targets_) + target.buffer.resize(samplesPerBlock); + + impl.temp_.resize(samplesPerBlock); +} + +ModMatrix::SourceId ModMatrix::registerSource(const ModKey& key, ModGenerator& gen) +{ + Impl& impl = *impl_; + + auto it = impl.sourceIndex_.find(key); + if (it != impl.sourceIndex_.end()) { + ASSERT(&gen == impl.sources_[it->second].gen); + return SourceId(it->second); + } + + SourceId id(static_cast(impl.sources_.size())); + impl.sources_.emplace_back(); + + Impl::Source &source = impl.sources_.back(); + source.key = key; + source.gen = &gen; + source.bufferReady = false; + source.buffer.resize(impl.samplesPerBlock_); + + impl.sourceIndex_[key] = id.number(); + + gen.setSampleRate(impl.sampleRate_); + gen.setSamplesPerBlock(impl.samplesPerBlock_); + + return id; +} + +ModMatrix::TargetId ModMatrix::registerTarget(const ModKey& key) +{ + Impl& impl = *impl_; + + auto it = impl.targetIndex_.find(key); + if (it != impl.targetIndex_.end()) + return TargetId(it->second); + + TargetId id(static_cast(impl.targets_.size())); + impl.targets_.emplace_back(); + + Impl::Target &target = impl.targets_.back(); + target.key = key; + target.bufferReady = false; + target.buffer.resize(impl.samplesPerBlock_); + + impl.targetIndex_[key] = id.number(); + return id; +} + +ModMatrix::SourceId ModMatrix::findSource(const ModKey& key) +{ + Impl& impl = *impl_; + + auto it = impl.sourceIndex_.find(key); + if (it == impl.sourceIndex_.end()) + return {}; + + return SourceId(it->second); +} + +ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key) +{ + Impl& impl = *impl_; + + auto it = impl.targetIndex_.find(key); + if (it == impl.targetIndex_.end()) + return {}; + + return TargetId(it->second); +} + +bool ModMatrix::connect(SourceId sourceId, TargetId targetId) +{ + Impl& impl = *impl_; + unsigned sourceIndex = sourceId.number(); + unsigned targetIndex = targetId.number(); + + if (sourceIndex >= impl.sources_.size() || targetIndex >= impl.targets_.size()) + return false; + + Impl::Target& target = impl.targets_[targetIndex]; + /*Impl::ConnectionData& conn =*/ target.connectedSources[sourceIndex]; + + return true; +} + +void ModMatrix::init() +{ + Impl& impl = *impl_; + + for (Impl::Source &source : impl.sources_) { + int flags = source.key.flags(); + if (flags & kModIsPerCycle) + source.gen->init(source.key, {}); + } +} + +void ModMatrix::initVoice(NumericId voiceId) +{ + Impl& impl = *impl_; + + for (Impl::Source &source : impl.sources_) { + int flags = source.key.flags(); + if (flags & kModIsPerVoice) + source.gen->init(source.key, voiceId); + } +} + +void ModMatrix::beginCycle(unsigned numFrames) +{ + Impl& impl = *impl_; + + impl.numFrames_ = numFrames; + + for (Impl::Source &source : impl.sources_) + source.bufferReady = false; + for (Impl::Target &target : impl.targets_) + target.bufferReady = false; +} + +void ModMatrix::beginVoice(NumericId voiceId) +{ + Impl& impl = *impl_; + + impl.voiceId_ = voiceId; + + for (Impl::Source &source : impl.sources_) { + const int flags = source.key.flags(); + if (flags & kModIsPerVoice) + source.bufferReady = false; + } + for (Impl::Target &target : impl.targets_) { + const int flags = target.key.flags(); + if (flags & kModIsPerVoice) + target.bufferReady = false; + } +} + +float* ModMatrix::getModulation(TargetId targetId) +{ + if (!validTarget(targetId)) + return nullptr; + + Impl& impl = *impl_; + const uint32_t targetIndex = targetId.number(); + Impl::Target &target = impl.targets_[targetIndex]; + const int flags = target.key.flags(); + + const uint32_t numFrames = impl.numFrames_; + absl::Span buffer(target.buffer.data(), numFrames); + + // check if already processed + if (target.bufferReady) + return buffer.data(); + + // set the ready flag to prevent a cycle + // in case there is, be sure to initialize the buffer + target.bufferReady = true; + if (flags & kModIsMultiplicative) + sfz::fill(buffer, 1.0f); + else if (flags & kModIsPercentMultiplicative) + sfz::fill(buffer, 100.0f); + else { + ASSERT(flags & kModIsAdditive); + sfz::fill(buffer, 0.0f); + } + + auto sourcesPos = target.connectedSources.begin(); + auto sourcesEnd = target.connectedSources.end(); + + // generate the first source in buffer + if (sourcesPos != sourcesEnd) { + Impl::Source &source = impl.sources_[sourcesPos->first]; + source.gen->generate(source.key, impl.voiceId_, buffer); + ++sourcesPos; + } + + // generate next sources in temporary buffer + // then add or multiply, depending on target flags + absl::Span temp(impl.temp_.data(), numFrames); + while (sourcesPos != sourcesEnd) { + Impl::Source &source = impl.sources_[sourcesPos->first]; + source.gen->generate(source.key, impl.voiceId_, temp); + if (flags & kModIsMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= temp[i]; + } + else if (flags & kModIsPercentMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= 0.01f * temp[i]; + } + else { + ASSERT(flags & kModIsAdditive); + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] += temp[i]; + } + ++sourcesPos; + } + + return buffer.data(); +} + +bool ModMatrix::validTarget(TargetId id) const +{ + return static_cast(id.number()) < impl_->targets_.size(); +} + +bool ModMatrix::validSource(SourceId id) const +{ + return static_cast(id.number()) < impl_->sources_.size(); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h new file mode 100644 index 00000000..34edb802 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.h @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../NumericId.h" +#include +#include + +namespace sfz { + +class ModKey; +class ModGenerator; +class Voice; + +/** + * @brief Modulation matrix + */ +class ModMatrix { +public: + ModMatrix(); + ~ModMatrix(); + + struct SourceIdTag; + struct TargetIdTag; + + //! Identifier of a modulation source + typedef NumericId SourceId; + + //! Identifier of a modulation target + typedef NumericId TargetId; + + /** + * @brief Reset the matrix to the empty state. + */ + void clear(); + + /** + * @brief Change the sample rate. + * + * @param sampleRate new sample rate + */ + void setSampleRate(double sampleRate); + + /** + * @brief Resize the modulation buffers. + * + * @param samplesPerBlock new block size + */ + void setSamplesPerBlock(unsigned samplesPerBlock); + + /** + * @brief Register a modulation source inside the matrix. + * If it is already present, it just returns the existing id. + * + * @param key source key + * @param gen generator + * @param flags source flags + */ + SourceId registerSource(const ModKey& key, ModGenerator& gen); + + /** + * @brief Register a modulation target inside the matrix. + * + * @param key target key + * @param region target region + * @param flags target flags + */ + TargetId registerTarget(const ModKey& key); + + /** + * @brief Look up a source by key. + * + * @param key source key + */ + SourceId findSource(const ModKey& key); + + /** + * @brief Look up a target by key. + * + * @param key target key + */ + TargetId findTarget(const ModKey& key); + + /** + * @brief Connect a source and a destination inside the matrix. + * + * @param sourceId source of the connection + * @param targetId target of the connection + * @return true if the connection was successfully made, otherwise false + */ + bool connect(SourceId sourceId, TargetId targetId); + + /** + * @brief Reinitialize modulation sources overall. + * This must be called once after setting up the matrix. + */ + void init(); + + /** + * @brief Reinitialize modulation source for a given voice. + * This must be called first after a voice enters active state. + */ + void initVoice(NumericId voiceId); + + /** + * @brief Start modulation processing for the entire cycle. + * This clears all the buffers. + * + * @param numFrames + */ + void beginCycle(unsigned numFrames); + + /** + * @brief Start modulation processing for a given voice. + * This clears all the buffers which are per-voice. + * + * @param voiceId the identifier of the current voice + */ + void beginVoice(NumericId voiceId); + + /** + * @brief Get the modulation buffer for the given target. + * If the target does not exist, the result is null. + * + * @param targetId identifier of the modulation target + */ + float* getModulation(TargetId targetId); + + /** + * @brief Get the modulation buffer for the given target. + * Same as `getModulation`, but accepting a key directly. + * + * @param targetKey key of the modulation target + */ + float* getModulationByKey(const ModKey& targetKey) + { return getModulation(findTarget(targetKey)); } + + /** + * @brief Return whether the target identifier is valid. + * + * @param id + */ + bool validTarget(TargetId id) const; + + /** + * @brief Return whether the source identifier is valid. + * + * @param id + */ + bool validSource(SourceId id) const; + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp new file mode 100644 index 00000000..b60269f0 --- /dev/null +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Controller.h" +#include "../ModKey.h" +#include "../../Smoothers.h" +#include "../../ModifierHelpers.h" +#include "../../Resources.h" +#include "../../Config.h" +#include "../../Debug.h" +#include + +namespace sfz { + +struct ControllerSource::Impl { + double sampleRate_ = config::defaultSampleRate; + Resources* res_ = nullptr; + absl::flat_hash_map smoother_; +}; + +ControllerSource::ControllerSource(Resources& res) + : impl_(new Impl) +{ + impl_->res_ = &res; +} + +ControllerSource::~ControllerSource() +{ +} + +void ControllerSource::setSampleRate(double sampleRate) +{ + if (impl_->sampleRate_ == sampleRate) + return; + + impl_->sampleRate_ = sampleRate; + + for (auto& item : impl_->smoother_) { + const ModKey::Parameters p = item.first.parameters(); + item.second.setSmoothing(p.smooth, sampleRate); + } +} + +void ControllerSource::setSamplesPerBlock(unsigned count) +{ + (void)count; +} + +void ControllerSource::init(const ModKey& sourceKey, NumericId voiceId) +{ + (void)voiceId; + + const ModKey::Parameters p = sourceKey.parameters(); + if (p.smooth > 0) { + Smoother s; + s.setSmoothing(p.smooth, impl_->sampleRate_); + impl_->smoother_[sourceKey] = s; + } + else { + impl_->smoother_.erase(sourceKey); + } +} + +void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + (void)voiceId; + + const ModKey::Parameters p = sourceKey.parameters(); + const Resources& res = *impl_->res_; + const Curve& curve = res.curves.getCurve(p.curve); + const MidiState& ms = res.midiState; + const EventVector& events = ms.getCCEvents(p.cc); + + auto transformValue = [p, &curve](float x) { + return curve.evalNormalized(x) * p.value; + }; + + if (p.step > 0.0f) + linearEnvelope(events, buffer, transformValue, p.step); + else + linearEnvelope(events, buffer, transformValue); + + auto it = impl_->smoother_.find(sourceKey); + if (it != impl_->smoother_.end()) { + Smoother& s = it->second; + + #pragma message("TODO: implement CC shortcut") + bool canShortcut = false; + + s.process(buffer, buffer, canShortcut); + } +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/Controller.h b/src/sfizz/modulations/sources/Controller.h new file mode 100644 index 00000000..1dfe26cd --- /dev/null +++ b/src/sfizz/modulations/sources/Controller.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../ModGenerator.h" +#include + +namespace sfz { + +struct Resources; + +class ControllerSource : public ModGenerator { +public: + explicit ControllerSource(Resources& res); + ~ControllerSource(); + void setSampleRate(double sampleRate) override; + void setSamplesPerBlock(unsigned count) override; + void init(const ModKey& sourceKey, NumericId voiceId) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 21bc72d1..9b7155c5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,6 +36,7 @@ set(SFIZZ_TEST_SOURCES SwapAndPopT.cpp TuningT.cpp ConcurrencyT.cpp + ModulationsT.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp new file mode 100644 index 00000000..3db0a96a --- /dev/null +++ b/tests/ModulationsT.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/modulations/ModId.h" +#include "sfizz/modulations/ModKey.h" +#include "catch2/catch.hpp" + +TEST_CASE("[Modulations] Identifiers") +{ + // check that modulations are well defined as either source and target + // and all targets have their default value defined + + sfz::ModIds::forEachSourceId([](sfz::ModId id) + { + REQUIRE(sfz::ModIds::isSource(id)); + REQUIRE(!sfz::ModIds::isTarget(id)); + }); + + sfz::ModIds::forEachTargetId([](sfz::ModId id) + { + REQUIRE(sfz::ModIds::isTarget(id)); + REQUIRE(!sfz::ModIds::isSource(id)); + }); +} + +TEST_CASE("[Modulations] Flags") +{ + // check validity of modulation flags + + static auto* checkBasicFlags = +[](int flags) + { + REQUIRE(flags != sfz::kModFlagsInvalid); + REQUIRE(((flags & sfz::kModIsPerCycle) ^ + (flags & sfz::kModIsPerVoice)) != 0); + }; + static auto* checkSourceFlags = +[](int flags) + { + checkBasicFlags(flags); + // nothing else + }; + static auto* checkTargetFlags = +[](int flags) + { + checkBasicFlags(flags); + REQUIRE(((flags & sfz::kModIsAdditive) ^ + (flags & sfz::kModIsMultiplicative) ^ + (flags & sfz::kModIsPercentMultiplicative)) != 0); + }; + + sfz::ModIds::forEachSourceId([](sfz::ModId id) + { + checkSourceFlags(sfz::ModIds::flags(id)); + }); + + sfz::ModIds::forEachTargetId([](sfz::ModId id) + { + checkTargetFlags(sfz::ModIds::flags(id)); + }); +} + +TEST_CASE("[Modulations] Display names") +{ + // check all modulations are implemented in `toString` + + sfz::ModIds::forEachSourceId([](sfz::ModId id) + { + REQUIRE(!sfz::ModKey(id).toString().empty()); + }); + + sfz::ModIds::forEachTargetId([](sfz::ModId id) + { + REQUIRE(!sfz::ModKey(id).toString().empty()); + }); +} From 0ebe24939d04f2620f09a87e5055dc81ca6b890c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 16:23:28 +0200 Subject: [PATCH 051/445] CC number oopsie --- src/sfizz/modulations/ModKey.cpp | 2 +- src/sfizz/modulations/ModKey.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 3a865bd7..fc60df9c 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -12,7 +12,7 @@ namespace sfz { -ModKey ModKey::createCC(uint8_t cc, uint8_t curve, uint8_t smooth, float value, float step) +ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step) { ModKey::Parameters p; p.cc = cc; diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index e70c244d..3fd5d470 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -27,7 +27,7 @@ public: explicit ModKey(ModId id, NumericId region = {}, Parameters params = {}) : id_(id), region_(region), params_(params) {} - static ModKey createCC(uint8_t cc, uint8_t curve, uint8_t smooth, float value, float step); + static ModKey createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step); static ModKey createNXYZ(ModId id, NumericId region, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0); explicit operator bool() const noexcept { return id_ != ModId(); } @@ -45,7 +45,7 @@ public: Parameters() { std::memset(this, 0, sizeof(*this)); } union { //! Parameters if this key identifies a CC source - struct { uint8_t cc, curve, smooth; float value, step; }; + struct { uint16_t cc; uint8_t curve, smooth; float value, step; }; //! Parameters otherwise, based on the related opcode // eg. `N` in `lfoN`, `N, X` in `lfoN_eqX` struct { uint8_t N, X, Y, Z; }; From 82ebb639a76f3eb98e2f9f88c3c4aac7a9256cbe Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 17:32:25 +0200 Subject: [PATCH 052/445] Remove unused code, update tests --- src/CMakeLists.txt | 1 - src/sfizz/ModifierHelpers.h | 78 +---------------- src/sfizz/Modifiers.h | 92 -------------------- src/sfizz/Opcode.h | 2 +- src/sfizz/Region.cpp | 39 ++------- src/sfizz/Region.h | 8 +- src/sfizz/Synth.cpp | 5 -- src/sfizz/Synth.h | 5 +- src/sfizz/Voice.cpp | 48 ---------- src/sfizz/Voice.h | 24 ----- tests/CMakeLists.txt | 2 + tests/EventEnvelopesT.cpp | 3 +- tests/FilesT.cpp | 11 ++- tests/RegionT.cpp | 169 +++++++++++++++++++----------------- tests/RegionTHelpers.cpp | 41 +++++++++ tests/RegionTHelpers.h | 28 ++++++ 16 files changed, 182 insertions(+), 374 deletions(-) delete mode 100644 src/sfizz/Modifiers.h create mode 100644 tests/RegionTHelpers.cpp create mode 100644 tests/RegionTHelpers.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f0343c5e..a1199157 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,7 +73,6 @@ set (SFIZZ_HEADERS sfizz/MathHelpers.h sfizz/MidiState.h sfizz/ModifierHelpers.h - sfizz/Modifiers.h sfizz/NumericId.h sfizz/OnePoleFilter.h sfizz/Oversampler.h diff --git a/src/sfizz/ModifierHelpers.h b/src/sfizz/ModifierHelpers.h index 609b16ff..e9b76361 100644 --- a/src/sfizz/ModifierHelpers.h +++ b/src/sfizz/ModifierHelpers.h @@ -8,8 +8,7 @@ #include "Range.h" #include "Defaults.h" -#include "Modifiers.h" -#include "Resources.h" +#include "SfzHelpers.h" #include "absl/types/span.h" namespace sfz { @@ -257,77 +256,4 @@ void pitchBendEnvelope(const EventVector& events, absl::Span envelope, F& multiplicativeEnvelope(events, envelope, std::forward(lambda)); } -/** - * @brief Builds a linear envelope, possibly quantized, based on the events fetched - * from a midi state and the modifier data. This is a helper function for recurrent - * code in the voice logic. - * - * @tparam F - * @param resources - * @param span - * @param ccData - * @param lambda - */ -template -void linearModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) -{ - const auto& events = resources.midiState.getCCEvents(ccData.cc); - const auto& curve = resources.curves.getCurve(ccData.data.curve); - if (ccData.data.step == 0.0f) { - linearEnvelope(events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }); - } else { - const float stepSize { lambda(ccData.data.step) }; - linearEnvelope( - events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }, - stepSize); - } -} - -/** - * @brief Builds a multiplicative envelope, possibly quantized, based on the events fetched - * from a midi state and the modifier data. This is a helper function for recurrent - * code in the voice logic. - * - * @tparam F - * @param resources - * @param span - * @param ccData - * @param lambda - */ -template -void multiplicativeModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData, F&& lambda) -{ - const auto& events = resources.midiState.getCCEvents(ccData.cc); - const auto& curve = resources.curves.getCurve(ccData.data.curve); - if (ccData.data.step == 0.0f) { - multiplicativeEnvelope(events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }); - } else { - const float stepSize { lambda(ccData.data.step) }; - multiplicativeEnvelope( - events, span, [&ccData, &curve, &lambda](float x) { - return lambda(curve.evalNormalized(x) * ccData.data.value); - }, - stepSize); - } -} - -/** - * @brief Alias for a simple linear modifier with no lambda - * - * @tparam F - * @param resources - * @param span - * @param ccData - * @param lambda - */ -inline void linearModifier(const sfz::Resources& resources, absl::Span span, const sfz::CCData& ccData) -{ - linearModifier(resources, span, ccData, [](float x) { return x; }); -} -} +} // namespace sfz diff --git a/src/sfizz/Modifiers.h b/src/sfizz/Modifiers.h deleted file mode 100644 index 4868d112..00000000 --- a/src/sfizz/Modifiers.h +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause - -// This code is part of the sfizz library and is licensed under a BSD 2-clause -// license. You should have receive a LICENSE.md file along with the code. -// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz - -#pragma once -#include "Config.h" -#include -#include -#include -#include -#include - -namespace sfz { - -/** - * @brief Base modifier class - * - */ -struct Modifier { - float value { 0.0f }; - float step { 0.0f }; - uint8_t curve { 0 }; - uint8_t smooth { 0 }; - static_assert(config::maxCurves - 1 <= std::numeric_limits::max(), "The curve type in the Modifier struct cannot support the required number of curves"); -}; - -enum class Mod : size_t { - amplitude = 0, - pan, - width, - position, - pitch, - volume, - sentinel -}; - -/** - * @brief Vectors of elements indexed on modifiers with casting and iterators - * - * @tparam T - */ -template -class ModifierVector : public std::vector { -public: - T& operator[](sfz::Mod idx) { return this->std::vector::operator[](static_cast(idx)); } - const T& operator[](sfz::Mod idx) const { return this->std::vector::operator[](static_cast(idx)); } -}; - -/** - * @brief Array of elements indexed on modifiers with casting and iterators - * - * @tparam T - */ -template -class ModifierArray { -public: - using ContainerType = typename std::array; - using iterator = typename ContainerType::iterator; - using const_iterator = typename ContainerType::const_iterator; - ModifierArray() = default; - ModifierArray(T val) - { - std::fill(underlying.begin(), underlying.end(), val); - } - ModifierArray(std::array&& array) : underlying(array) {} - T& operator[](sfz::Mod idx) { return underlying.operator[](static_cast(idx)); } - const T& operator[](sfz::Mod idx) const { return underlying.operator[](static_cast(idx)); } - iterator begin() { return underlying.begin(); } - iterator end() { return underlying.end(); } - const_iterator begin() const { return underlying.begin(); } - const_iterator end() const { return underlying.end(); } -private: - ContainerType underlying {}; -}; - -/** - * @brief Helper for iterating over all possible modifiers. - * Should fail at compile time if you update the modifiers but not this. - * - */ -static const ModifierArray allModifiers {{ - Mod::amplitude, - Mod::pan, - Mod::width, - Mod::position, - Mod::pitch, - Mod::volume -}}; - -} diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index 87b25666..4d15f844 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -13,7 +13,7 @@ #include "absl/types/optional.h" #include "absl/meta/type_traits.h" #include "absl/strings/ascii.h" -#include +#include "absl/strings/string_view.h" #include #include #include diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index c8775c0b..685d7587 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -379,35 +379,35 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, volume, Default::volumeRange); break; case_any_ccN("volume"): // also gain - processGenericCc(opcode, Default::volumeCCRange, &modifiers[Mod::volume], ModKey::createNXYZ(ModId::Volume, id)); + processGenericCc(opcode, Default::volumeCCRange, ModKey::createNXYZ(ModId::Volume, id)); break; case hash("amplitude"): if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) amplitude = normalizePercents(*value); break; case_any_ccN("amplitude"): - processGenericCc(opcode, Default::amplitudeRange, &modifiers[Mod::amplitude], ModKey::createNXYZ(ModId::Amplitude, id)); + processGenericCc(opcode, Default::amplitudeRange, ModKey::createNXYZ(ModId::Amplitude, id)); break; case hash("pan"): if (auto value = readOpcode(opcode.value, Default::panRange)) pan = normalizePercents(*value); break; case_any_ccN("pan"): - processGenericCc(opcode, Default::panCCRange, &modifiers[Mod::pan], ModKey::createNXYZ(ModId::Pan, id)); + processGenericCc(opcode, Default::panCCRange, ModKey::createNXYZ(ModId::Pan, id)); break; case hash("position"): if (auto value = readOpcode(opcode.value, Default::positionRange)) position = normalizePercents(*value); break; case_any_ccN("position"): - processGenericCc(opcode, Default::positionCCRange, &modifiers[Mod::position], ModKey::createNXYZ(ModId::Position, id)); + processGenericCc(opcode, Default::positionCCRange, ModKey::createNXYZ(ModId::Position, id)); break; case hash("width"): if (auto value = readOpcode(opcode.value, Default::widthRange)) width = normalizePercents(*value); break; case_any_ccN("width"): - processGenericCc(opcode, Default::widthCCRange, &modifiers[Mod::width], ModKey::createNXYZ(ModId::Width, id)); + processGenericCc(opcode, Default::widthCCRange, ModKey::createNXYZ(ModId::Width, id)); break; case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); @@ -771,7 +771,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, tune, Default::tuneRange); break; case_any_ccN("pitch"): // also tune - processGenericCc(opcode, Default::tuneCCRange, &modifiers[Mod::pitch], ModKey::createNXYZ(ModId::Pitch, id)); + processGenericCc(opcode, Default::tuneCCRange, ModKey::createNXYZ(ModId::Pitch, id)); break; case hash("bend_up"): // also bendup setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); @@ -925,7 +925,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return true; } -bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap, const ModKey& target) +bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, const ModKey& target) { if (!opcode.isAnyCcN()) return false; @@ -934,31 +934,6 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCM if (ccNumber >= config::numCCs) return false; - // TODO obsolete after implementing mod matrix - if (ccMap) { - Modifier& modifier = (*ccMap)[ccNumber]; - switch (opcode.category) { - case kOpcodeOnCcN: - setValueFromOpcode(opcode, modifier.value, range); - break; - case kOpcodeCurveCcN: - setValueFromOpcode(opcode, modifier.curve, Default::curveCCRange); - break; - case kOpcodeStepCcN: - { - const Range stepCCRange { 0.0f, std::max(std::abs(range.getStart()), std::abs(range.getEnd())) }; - setValueFromOpcode(opcode, modifier.step, stepCCRange); - } - break; - case kOpcodeSmoothCcN: - setValueFromOpcode(opcode, modifier.smooth, Default::smoothCCRange); - break; - default: - assert(false); - break; - } - } - if (target) { // search an existing connection of same CC number and target // if it exists, modify, otherwise create diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index da044fd9..0426d6d7 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -17,9 +17,9 @@ #include "MidiState.h" #include "FileId.h" #include "NumericId.h" -#include "Modifiers.h" #include "modulations/ModKey.h" #include "absl/types/optional.h" +#include "absl/strings/string_view.h" #include #include #include @@ -241,12 +241,11 @@ struct Region { * * @param opcode * @param range - * @param ccMap * @param target * @return true if the opcode was properly read and stored. * @return false */ - bool processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap, const ModKey& target); + bool processGenericCc(const Opcode& opcode, Range range, const ModKey& target); void offsetAllKeys(int offset) noexcept; @@ -370,9 +369,6 @@ struct Region { // Effects std::vector gainToEffect; - // Modifiers - ModifierArray> modifiers; - bool triggerOnCC { false }; // whether the region triggers on CC events or note events bool triggerOnNote { true }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 7bae9797..cb1ba8c4 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -452,7 +452,6 @@ void sfz::Synth::finalizeSfzLoad() size_t maxFilters { 0 }; size_t maxEQs { 0 }; - ModifierArray maxModifiers { 0 }; while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); @@ -563,8 +562,6 @@ void sfz::Synth::finalizeSfzLoad() region->registerTempo(2.0f); maxFilters = max(maxFilters, region->filters.size()); maxEQs = max(maxEQs, region->equalizers.size()); - for (const auto& mod : allModifiers) - maxModifiers[mod] = max(maxModifiers[mod], region->modifiers[mod].size()); ++currentRegionIndex; } @@ -574,7 +571,6 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; - settingsPerVoice.maxModifiers = maxModifiers; applySettingsPerVoice(); @@ -1346,7 +1342,6 @@ void sfz::Synth::applySettingsPerVoice() for (auto& voice : voices) { voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); - voice->prepareSmoothers(settingsPerVoice.maxModifiers); } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 5c41078d..8133b676 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -18,11 +18,11 @@ #include "parser/Parser.h" #include "VoiceStealing.h" #include "utility/SpinMutex.h" -#include "absl/types/span.h" +#include #include +#include #include #include -#include #include namespace sfz { @@ -772,7 +772,6 @@ private: struct SettingsPerVoice { size_t maxFilters { 0 }; size_t maxEQs { 0 }; - ModifierArray maxModifiers { 0 }; }; SettingsPerVoice settingsPerVoice; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a9ced8b5..9f1b9197 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -141,33 +141,6 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate); - for (auto& modId : allModifiers) { - ASSERT(modifierSmoothers[modId].size() >= region->modifiers[modId].size()); - forEachWithSmoother(modId, [modId, this](const CCData& mod, Smoother& smoother) { - const auto ccValue = resources.midiState.getCCValue(mod.cc); - const auto& curve = resources.curves.getCurve(mod.data.curve); - const auto finalValue = curve.evalNormalized(ccValue) * mod.data.value; - switch (modId) { - case Mod::volume: - smoother.reset(db2mag(finalValue)); - break; - case Mod::pitch: - smoother.reset(centsFactor(finalValue)); - break; - case Mod::amplitude: - case Mod::pan: - case Mod::width: - case Mod::position: - smoother.reset(normalizePercents(finalValue)); - break; - default: - smoother.reset(finalValue); - break; - } - smoother.setSmoothing(mod.data.smooth, sampleRate); - }); - } - resources.modMatrix.initVoice(id); } @@ -882,12 +855,6 @@ void sfz::Voice::switchState(State s) } } -void sfz::Voice::prepareSmoothers(const ModifierArray& numModifiers) -{ - for (auto& mod : allModifiers) - modifierSmoothers[mod].resize(numModifiers[mod]); -} - void sfz::Voice::pitchEnvelope(absl::Span pitchSpan) noexcept { const auto numFrames = pitchSpan.size(); @@ -918,21 +885,6 @@ void sfz::Voice::pitchEnvelope(absl::Span pitchSpan) noexcept void sfz::Voice::resetSmoothers() noexcept { - for (auto& mod : allModifiers) { - const auto resetValue = [mod] { - switch (mod) { - case Mod::volume: // fallthrough - case Mod::pitch: - return 1.0f; - default: - return 0.0f; - } - }(); - - for (auto& smoother : modifierSmoothers[mod]) { - smoother.reset(resetValue); - } - } bendSmoother.reset(1.0f); gainSmoother.reset(0.0f); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index f567c5e6..52a7f0c8 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -302,8 +302,6 @@ public: Duration getLastFilterDuration() const noexcept { return filterDuration; } Duration getLastPanningDuration() const noexcept { return panningDuration; } - void prepareSmoothers(const ModifierArray& numModifiers); - private: /** * @brief Fill a span with data from a file source. This is the first step @@ -390,27 +388,6 @@ private: */ void removeVoiceFromRing() noexcept; - /** - * @brief Helper function to iterate jointly on modifiers and smoothers - * for a given modulation target of type sfz::Mod - * - * @tparam F - * @param modId - * @param lambda - */ - template - void forEachWithSmoother(sfz::Mod modId, F&& lambda) - { - size_t count = region->modifiers[modId].size(); - ASSERT(modifierSmoothers[modId].size() >= count); - auto mod = region->modifiers[modId].begin(); - auto smoother = modifierSmoothers[modId].begin(); - for (size_t i = 0; i < count; ++i) { - lambda(*mod, *smoother); - incrementAll(mod, smoother); - } - } - /** * @brief Initialize frequency and gain coefficients for the oscillators. */ @@ -479,7 +456,6 @@ private: fast_real_distribution uniformNoiseDist { -config::uniformNoiseBounds, config::uniformNoiseBounds }; fast_gaussian_generator gaussianNoiseDist { 0.0f, config::noiseVariance }; - ModifierArray> modifierSmoothers; Smoother gainSmoother; Smoother bendSmoother; Smoother xfadeSmoother; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9b7155c5..4018edee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,8 @@ project(sfizz) set(SFIZZ_TEST_SOURCES RegionT.cpp + RegionTHelpers.h + RegionTHelpers.cpp ParsingT.cpp HelpersT.cpp HelpersT.cpp diff --git a/tests/EventEnvelopesT.cpp b/tests/EventEnvelopesT.cpp index 64bcb7e4..01287d3e 100644 --- a/tests/EventEnvelopesT.cpp +++ b/tests/EventEnvelopesT.cpp @@ -261,6 +261,7 @@ TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps") REQUIRE(approxEqual(output, expected)); } +#if 0 TEST_CASE("[linearModifiers] Compare with envelopes") { sfz::Resources resources; @@ -360,4 +361,4 @@ TEST_CASE("[multiplicativeModifiers] Compare with envelopes") }); REQUIRE(approxEqual(output, envelope)); } - +#endif diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 73ea4ab0..b47181fe 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -4,8 +4,11 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +#include "RegionTHelpers.h" #include "sfizz/Synth.h" #include "sfizz/SfzHelpers.h" +#include "sfizz/modulations/ModId.h" +#include "sfizz/modulations/ModKey.h" #include "catch2/catch.hpp" #include "ghc/fs_std.hpp" #if defined(__APPLE__) @@ -356,9 +359,11 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines") REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 ); REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 ); - REQUIRE(!synth.getRegionView(2)->modifiers[Mod::amplitude].empty()); - REQUIRE(synth.getRegionView(2)->modifiers[Mod::amplitude].contains(10)); - REQUIRE(synth.getRegionView(2)->modifiers[Mod::amplitude].getWithDefault(10).value == 34.0f); + + const ModKey target = ModKey::createNXYZ(ModId::Amplitude, synth.getRegionView(2)->getId()); + const RegionCCView view(*synth.getRegionView(2), target); + REQUIRE(!view.empty()); + REQUIRE(view.at(10).value == 34.0f); } TEST_CASE("[Files] Specific bug: relative path with backslashes") diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index ecfea499..472ab8fc 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -4,10 +4,14 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +#include "RegionTHelpers.h" #include "sfizz/MidiState.h" #include "sfizz/Region.h" #include "sfizz/SfzHelpers.h" +#include "sfizz/modulations/ModId.h" +#include "sfizz/modulations/ModKey.h" #include "catch2/catch.hpp" +#include using namespace Catch::literals; using namespace sfz::literals; using namespace sfz; @@ -541,28 +545,29 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("pan_oncc") { - REQUIRE(region.modifiers[Mod::pan].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Pan, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "pan_oncc45", "4.2" }); - REQUIRE(region.modifiers[Mod::pan].contains(45)); - REQUIRE(region.modifiers[Mod::pan][45].value == 4.2_a); + REQUIRE(view.at(45).value == 4.2_a); region.parseOpcode({ "pan_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::pan][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "pan_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::pan][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "pan_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::pan][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "pan_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::pan][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "pan_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::pan][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "pan_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::pan][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "pan_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::pan][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "pan_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::pan][120].step == 200.0_a); + REQUIRE(view.at(120).step == 200.0_a); region.parseOpcode({ "pan_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::pan][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("width") @@ -580,28 +585,29 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("width_oncc") { - REQUIRE(region.modifiers[Mod::width].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Width, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "width_oncc45", "4.2" }); - REQUIRE(region.modifiers[Mod::width].contains(45)); - REQUIRE(region.modifiers[Mod::width][45].value == 4.2_a); + REQUIRE(view.at(45).value == 4.2_a); region.parseOpcode({ "width_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::width][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "width_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::width][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "width_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::width][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "width_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::width][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "width_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::width][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "width_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::width][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "width_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::width][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "width_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::width][120].step == 200.0_a); + REQUIRE(view.at(120).step == 200.0_a); region.parseOpcode({ "width_stepcc120", "-20" }); - REQUIRE(region.modifiers[Mod::width][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("position") @@ -619,28 +625,29 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("position_oncc") { - REQUIRE(region.modifiers[Mod::position].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Position, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "position_oncc45", "4.2" }); - REQUIRE(region.modifiers[Mod::position].contains(45)); - REQUIRE(region.modifiers[Mod::position][45].value == 4.2_a); + REQUIRE(view.at(45).value == 4.2_a); region.parseOpcode({ "position_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::position][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "position_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::position][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "position_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::position][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "position_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::position][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "position_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::position][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "position_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::position][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "position_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::position][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "position_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::position][120].step == 200.0_a); + REQUIRE(view.at(120).step == 200.0_a); region.parseOpcode({ "position_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::position][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("amp_keycenter") @@ -1641,95 +1648,93 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("amplitude_cc") { - REQUIRE(region.modifiers[Mod::amplitude].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Amplitude, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "amplitude_cc1", "40" }); - REQUIRE(region.modifiers[Mod::amplitude].contains(1)); - REQUIRE(region.modifiers[Mod::amplitude][1].value == 40.0_a); + REQUIRE(view.at(1).value == 40.0_a); region.parseOpcode({ "amplitude_oncc2", "30" }); - REQUIRE(region.modifiers[Mod::amplitude].contains(2)); - REQUIRE(region.modifiers[Mod::amplitude][2].value == 30.0_a); + REQUIRE(view.at(2).value == 30.0_a); region.parseOpcode({ "amplitude_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::amplitude][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "amplitude_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::amplitude][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "amplitude_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::amplitude][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "amplitude_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "amplitude_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "amplitude_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::amplitude][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "amplitude_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::amplitude][120].step == 24.0_a); + REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "amplitude_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::amplitude][120].step == 100.0_a); + REQUIRE(view.at(120).step == 100.0_a); region.parseOpcode({ "amplitude_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::amplitude][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("volume_oncc/gain_cc") { - REQUIRE(region.modifiers[Mod::volume].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Volume, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "gain_cc1", "40" }); - REQUIRE(region.modifiers[Mod::volume].contains(1)); - REQUIRE(region.modifiers[Mod::volume][1].value == 40_a); + REQUIRE(view.at(1).value == 40_a); region.parseOpcode({ "volume_oncc2", "-76" }); - REQUIRE(region.modifiers[Mod::volume].contains(2)); - REQUIRE(region.modifiers[Mod::volume][2].value == -76.0_a); + REQUIRE(view.at(2).value == -76.0_a); region.parseOpcode({ "gain_oncc4", "-1" }); - REQUIRE(region.modifiers[Mod::volume].contains(4)); - REQUIRE(region.modifiers[Mod::volume][4].value == -1.0_a); + REQUIRE(view.at(4).value == -1.0_a); region.parseOpcode({ "volume_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::volume][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "volume_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::volume][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "volume_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::volume][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "volume_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::volume][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "volume_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::volume][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "volume_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::volume][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "volume_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::volume][120].step == 24.0f); + REQUIRE(view.at(120).step == 24.0f); region.parseOpcode({ "volume_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::volume][120].step == 144.0f); + REQUIRE(view.at(120).step == 144.0f); region.parseOpcode({ "volume_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::volume][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } SECTION("tune_cc/pitch_cc") { - REQUIRE(region.modifiers[Mod::pitch].empty()); + const ModKey target = ModKey::createNXYZ(ModId::Pitch, region.getId()); + const RegionCCView view(region, target); + REQUIRE(view.empty()); region.parseOpcode({ "pitch_cc1", "40" }); - REQUIRE(region.modifiers[Mod::pitch].contains(1)); - REQUIRE(region.modifiers[Mod::pitch][1].value == 40.0); + REQUIRE(view.at(1).value == 40.0); region.parseOpcode({ "tune_oncc2", "-76" }); - REQUIRE(region.modifiers[Mod::pitch].contains(2)); - REQUIRE(region.modifiers[Mod::pitch][2].value == -76.0); + REQUIRE(view.at(2).value == -76.0); region.parseOpcode({ "pitch_oncc4", "-1" }); - REQUIRE(region.modifiers[Mod::pitch].contains(4)); - REQUIRE(region.modifiers[Mod::pitch][4].value == -1.0); + REQUIRE(view.at(4).value == -1.0); region.parseOpcode({ "tune_curvecc17", "18" }); - REQUIRE(region.modifiers[Mod::pitch][17].curve == 18); + REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "pitch_curvecc17", "15482" }); - REQUIRE(region.modifiers[Mod::pitch][17].curve == 255); + REQUIRE(view.at(17).curve == 255); region.parseOpcode({ "tune_curvecc17", "-2" }); - REQUIRE(region.modifiers[Mod::pitch][17].curve == 0); + REQUIRE(view.at(17).curve == 0); region.parseOpcode({ "pitch_smoothcc14", "85" }); - REQUIRE(region.modifiers[Mod::pitch][14].smooth == 85); + REQUIRE(view.at(14).smooth == 85); region.parseOpcode({ "tune_smoothcc14", "15482" }); - REQUIRE(region.modifiers[Mod::pitch][14].smooth == 100); + REQUIRE(view.at(14).smooth == 100); region.parseOpcode({ "pitch_smoothcc14", "-2" }); - REQUIRE(region.modifiers[Mod::pitch][14].smooth == 0); + REQUIRE(view.at(14).smooth == 0); region.parseOpcode({ "tune_stepcc120", "24" }); - REQUIRE(region.modifiers[Mod::pitch][120].step == 24.0f); + REQUIRE(view.at(120).step == 24.0f); region.parseOpcode({ "pitch_stepcc120", "15482" }); - REQUIRE(region.modifiers[Mod::pitch][120].step == 9600.0f); + REQUIRE(view.at(120).step == 9600.0f); region.parseOpcode({ "tune_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::pitch][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } } diff --git a/tests/RegionTHelpers.cpp b/tests/RegionTHelpers.cpp new file mode 100644 index 00000000..d55f05ad --- /dev/null +++ b/tests/RegionTHelpers.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "RegionTHelpers.h" +#include "sfizz/modulations/ModId.h" + +size_t RegionCCView::size() const +{ + size_t count = 0; + for (const sfz::Region::Connection& conn : region_.connections) + count += match(conn); + return count; +} + +bool RegionCCView::empty() const +{ + for (const sfz::Region::Connection& conn : region_.connections) + if (match(conn)) + return false; + return true; +} + +sfz::ModKey::Parameters RegionCCView::at(int cc) const +{ + for (const sfz::Region::Connection& conn : region_.connections) { + if (match(conn)) { + const sfz::ModKey::Parameters p = conn.first.parameters(); + if (p.cc == cc) + return p; + } + } + throw std::out_of_range("Region CC"); +} + +bool RegionCCView::match(const sfz::Region::Connection& conn) const +{ + return conn.first.id() == sfz::ModId::Controller && conn.second == target_; +} diff --git a/tests/RegionTHelpers.h b/tests/RegionTHelpers.h new file mode 100644 index 00000000..e9fcf897 --- /dev/null +++ b/tests/RegionTHelpers.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "sfizz/Region.h" +#include "sfizz/modulations/ModKey.h" + +class RegionCCView { +public: + RegionCCView(const sfz::Region& region, sfz::ModKey target) + : region_(region), target_(target) + { + } + + size_t size() const; + bool empty() const; + sfz::ModKey::Parameters at(int cc) const; + +private: + bool match(const sfz::Region::Connection& conn) const; + +private: + const sfz::Region& region_; + sfz::ModKey target_; +}; From efd616ffdfa03253473d11a9f313ced6c45185e0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 18:38:36 +0200 Subject: [PATCH 053/445] Ensure all ModKey to have unused content set to zero --- src/sfizz/modulations/ModKey.cpp | 32 +++++++++++++++++++++++++++++++- src/sfizz/modulations/ModKey.h | 12 ++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index fc60df9c..57038748 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -12,6 +12,36 @@ namespace sfz { +ModKey::Parameters::Parameters() noexcept +{ + // zero-fill the structure + // 1. this ensures that non-used values will be always 0 + // 2. this makes the object memcmp-comparable + std::memset(this, 0, sizeof(*this)); +} + +ModKey::Parameters::Parameters(const Parameters& other) noexcept +{ + std::memcpy(this, &other, sizeof(*this)); +} + +ModKey::Parameters& ModKey::Parameters::operator=(const Parameters& other) noexcept +{ + if (this != &other) + std::memcpy(this, &other, sizeof(*this)); + return *this; +} + +bool ModKey::Parameters::operator==(const Parameters& other) const noexcept +{ + return std::memcmp(this, &other, sizeof(*this)) == 0; +} + +bool ModKey::Parameters::operator!=(const Parameters& other) const noexcept +{ + return std::memcmp(this, &other, sizeof(*this)) != 0; +} + ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step) { ModKey::Parameters p; @@ -84,7 +114,7 @@ std::string ModKey::toString() const bool sfz::ModKey::operator==(const ModKey &other) const noexcept { return id_ == other.id_ && region_ && other.region_ && - !std::memcmp(¶meters(), &other.parameters(), sizeof(ModKey::Parameters)); + parameters() == other.parameters(); } bool sfz::ModKey::operator!=(const ModKey &other) const noexcept diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 3fd5d470..48977537 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -8,7 +8,6 @@ #include "ModKeyHash.h" #include "../NumericId.h" #include -#include namespace sfz { @@ -42,7 +41,16 @@ public: std::string toString() const; struct Parameters { - Parameters() { std::memset(this, 0, sizeof(*this)); } + Parameters() noexcept; + Parameters(const Parameters& other) noexcept; + Parameters& operator=(const Parameters& other) noexcept; + + Parameters(Parameters&&) = delete; + Parameters &operator=(Parameters&&) = delete; + + bool operator==(const Parameters& other) const noexcept; + bool operator!=(const Parameters& other) const noexcept; + union { //! Parameters if this key identifies a CC source struct { uint16_t cc; uint8_t curve, smooth; float value, step; }; From 6bb4b7c83e86442513b507905b0b62ad825c6914 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 18:47:09 +0200 Subject: [PATCH 054/445] Fix equality comparison --- src/sfizz/modulations/ModKey.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 57038748..69d8750f 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -113,7 +113,7 @@ std::string ModKey::toString() const bool sfz::ModKey::operator==(const ModKey &other) const noexcept { - return id_ == other.id_ && region_ && other.region_ && + return id_ == other.id_ && region_ == other.region_ && parameters() == other.parameters(); } From 97702e43081c0a2fdfea3bce0ad4b91a0bb25422 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 19:14:30 +0200 Subject: [PATCH 055/445] Advance the generator when not used --- src/sfizz/Synth.cpp | 8 +++++-- src/sfizz/modulations/ModGenerator.h | 14 ++++++++++++ src/sfizz/modulations/ModMatrix.cpp | 33 ++++++++++++++++++++++++++++ src/sfizz/modulations/ModMatrix.h | 12 ++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index cb1ba8c4..cba7e139 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -723,6 +723,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept } ModMatrix& mm = resources.modMatrix; + mm.beginCycle(numFrames); activeVoices = 0; { // Main render block @@ -731,8 +732,6 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept tempMixSpan->fill(0.0f); resources.filePool.cleanupPromises(); - mm.beginCycle(numFrames); - // Ramp out whatever is in the buffer at this point; should only be killed voice data linearRamp(*rampSpan, 1.0f, -1.0f / static_cast(numFrames)); for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { @@ -754,6 +753,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept callbackBreakdown.filters += voice->getLastFilterDuration(); callbackBreakdown.panning += voice->getLastPanningDuration(); + mm.endVoice(); + if (voice->toBeCleanedUp()) voice->reset(); } @@ -781,6 +782,9 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept // Apply the master volume buffer.applyGain(db2mag(volume)); + // Perform any remaining modulators + mm.endCycle(); + { // Clear events and advance midi time ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.advanceTime(buffer.getNumFrames()); diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h index c146593d..457460c3 100644 --- a/src/sfizz/modulations/ModGenerator.h +++ b/src/sfizz/modulations/ModGenerator.h @@ -47,6 +47,20 @@ public: * @param buffer output buffer */ virtual void generate(const ModKey& sourceKey, NumericId voiceNum, absl::Span buffer) = 0; + + /** + * @brief Advance the generator by a number of frames + * This is called instead of `generate` in case the output is discarded. + * It can be overriden with a faster implementation if wanted. + * + * @param sourceKey source key + * @param voiceNum voice number if the generator is per-voice, otherwise undefined + * @param buffer writable spare buffer, contents will be discarded + */ + virtual void generateDiscarded(const ModKey& sourceKey, NumericId voiceNum, absl::Span buffer) + { + generate(sourceKey, voiceNum, buffer); + } }; } // namespace sfz diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index dd1bd4c3..9dd94893 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -224,6 +224,22 @@ void ModMatrix::beginCycle(unsigned numFrames) target.bufferReady = false; } +void ModMatrix::endCycle() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + + for (Impl::Source &source : impl.sources_) { + if (!source.bufferReady) { + int flags = source.key.flags(); + if (flags & kModIsPerCycle) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, {}, buffer); + } + } + } +} + void ModMatrix::beginVoice(NumericId voiceId) { Impl& impl = *impl_; @@ -242,6 +258,23 @@ void ModMatrix::beginVoice(NumericId voiceId) } } +void ModMatrix::endVoice() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + const NumericId voiceId = impl.voiceId_; + + for (Impl::Source &source : impl.sources_) { + if (!source.bufferReady) { + int flags = source.key.flags(); + if (flags & kModIsPerVoice) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, voiceId, buffer); + } + } + } +} + float* ModMatrix::getModulation(TargetId targetId) { if (!validTarget(targetId)) diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 34edb802..e3381930 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -113,6 +113,12 @@ public: */ void beginCycle(unsigned numFrames); + /** + * @brief End modulation processing for the entire cycle. + * This performs a dummy run of any unused modulations. + */ + void endCycle(); + /** * @brief Start modulation processing for a given voice. * This clears all the buffers which are per-voice. @@ -121,6 +127,12 @@ public: */ void beginVoice(NumericId voiceId); + /** + * @brief End modulation processing for a given voice. + * This performs a dummy run of any unused modulations which are per-cycle. + */ + void endVoice(); + /** * @brief Get the modulation buffer for the given target. * If the target does not exist, the result is null. From 5835255d07bfa7c7023e6e334e05885ada88f09f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 20:19:22 +0200 Subject: [PATCH 056/445] Fix for macOS --- src/sfizz/modulations/ModKeyHash.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/modulations/ModKeyHash.h b/src/sfizz/modulations/ModKeyHash.h index a5cdf4c6..1f3ecbdc 100644 --- a/src/sfizz/modulations/ModKeyHash.h +++ b/src/sfizz/modulations/ModKeyHash.h @@ -5,12 +5,12 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include #include namespace sfz { class ModKey; } namespace std { - template struct hash; template <> struct hash { size_t operator()(const sfz::ModKey &key) const; }; From 652d0c898d84ee9380205ec857c734316ac8964e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 27 Jul 2020 21:00:13 +0200 Subject: [PATCH 057/445] Enable smoothing for CC --- src/sfizz/modulations/sources/Controller.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp index b60269f0..692b7556 100644 --- a/src/sfizz/modulations/sources/Controller.cpp +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -86,10 +86,7 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceI auto it = impl_->smoother_.find(sourceKey); if (it != impl_->smoother_.end()) { Smoother& s = it->second; - - #pragma message("TODO: implement CC shortcut") - bool canShortcut = false; - + bool canShortcut = events.size() == 1; s.process(buffer, buffer, canShortcut); } } From 0507dab7c4dc3fb7bf30c159792efc07873d92fd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 13:30:10 +0200 Subject: [PATCH 058/445] Ensure to only generate per-voice modulations of the same region --- src/sfizz/Synth.cpp | 2 +- src/sfizz/modulations/ModMatrix.cpp | 99 ++++++++++++++++++----------- src/sfizz/modulations/ModMatrix.h | 4 +- 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index cba7e139..61fb532d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -744,7 +744,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; - mm.beginVoice(voice->getId()); + mm.beginVoice(voice->getId(), voice->getRegion()->getId()); activeVoices++; renderVoiceToOutputs(*voice, *tempSpan); diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 9dd94893..74fa25cd 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -24,6 +24,7 @@ struct ModMatrix::Impl { uint32_t numFrames_ {}; NumericId voiceId_ {}; + NumericId regionId_ {}; struct Source { ModKey key; @@ -195,7 +196,7 @@ void ModMatrix::init() Impl& impl = *impl_; for (Impl::Source &source : impl.sources_) { - int flags = source.key.flags(); + const int flags = source.key.flags(); if (flags & kModIsPerCycle) source.gen->init(source.key, {}); } @@ -206,7 +207,7 @@ void ModMatrix::initVoice(NumericId voiceId) Impl& impl = *impl_; for (Impl::Source &source : impl.sources_) { - int flags = source.key.flags(); + const int flags = source.key.flags(); if (flags & kModIsPerVoice) source.gen->init(source.key, voiceId); } @@ -231,20 +232,23 @@ void ModMatrix::endCycle() for (Impl::Source &source : impl.sources_) { if (!source.bufferReady) { - int flags = source.key.flags(); + const int flags = source.key.flags(); if (flags & kModIsPerCycle) { absl::Span buffer(source.buffer.data(), numFrames); source.gen->generateDiscarded(source.key, {}, buffer); } } } + + impl.numFrames_ = 0; } -void ModMatrix::beginVoice(NumericId voiceId) +void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) { Impl& impl = *impl_; impl.voiceId_ = voiceId; + impl.regionId_ = regionId; for (Impl::Source &source : impl.sources_) { const int flags = source.key.flags(); @@ -263,16 +267,20 @@ void ModMatrix::endVoice() Impl& impl = *impl_; const uint32_t numFrames = impl.numFrames_; const NumericId voiceId = impl.voiceId_; + const NumericId regionId = impl.regionId_; for (Impl::Source &source : impl.sources_) { if (!source.bufferReady) { - int flags = source.key.flags(); - if (flags & kModIsPerVoice) { + const int flags = source.key.flags(); + if ((flags & kModIsPerVoice) && source.key.region() == regionId) { absl::Span buffer(source.buffer.data(), numFrames); source.gen->generateDiscarded(source.key, voiceId, buffer); } } } + + impl.voiceId_ = {}; + impl.regionId_ = {}; } float* ModMatrix::getModulation(TargetId targetId) @@ -281,13 +289,18 @@ float* ModMatrix::getModulation(TargetId targetId) return nullptr; Impl& impl = *impl_; + const NumericId regionId = impl.regionId_; const uint32_t targetIndex = targetId.number(); Impl::Target &target = impl.targets_[targetIndex]; - const int flags = target.key.flags(); + const int targetFlags = target.key.flags(); const uint32_t numFrames = impl.numFrames_; absl::Span buffer(target.buffer.data(), numFrames); + // only accept per-voice targets of the same region + if ((targetFlags & kModIsPerVoice) && regionId != target.key.region()) + return nullptr; + // check if already processed if (target.bufferReady) return buffer.data(); @@ -295,45 +308,59 @@ float* ModMatrix::getModulation(TargetId targetId) // set the ready flag to prevent a cycle // in case there is, be sure to initialize the buffer target.bufferReady = true; - if (flags & kModIsMultiplicative) - sfz::fill(buffer, 1.0f); - else if (flags & kModIsPercentMultiplicative) - sfz::fill(buffer, 100.0f); - else { - ASSERT(flags & kModIsAdditive); - sfz::fill(buffer, 0.0f); - } auto sourcesPos = target.connectedSources.begin(); auto sourcesEnd = target.connectedSources.end(); + bool isFirstSource = true; - // generate the first source in buffer - if (sourcesPos != sourcesEnd) { + // generate first source in output buffer, next sources in temporary buffer + // then add or multiply, depending on target flags + while (sourcesPos != sourcesEnd) { Impl::Source &source = impl.sources_[sourcesPos->first]; - source.gen->generate(source.key, impl.voiceId_, buffer); + const int sourceFlags = source.key.flags(); + + // only accept per-voice sources of the same region + bool useThisSource = true; + if (sourceFlags & kModIsPerVoice) + useThisSource = (regionId == source.key.region()); + + if (useThisSource) { + if (isFirstSource) { + source.gen->generate(source.key, impl.voiceId_, buffer); + isFirstSource = false; + } + else { + absl::Span temp(impl.temp_.data(), numFrames); + source.gen->generate(source.key, impl.voiceId_, temp); + if (targetFlags & kModIsMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= temp[i]; + } + else if (targetFlags & kModIsPercentMultiplicative) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= 0.01f * temp[i]; + } + else { + ASSERT(targetFlags & kModIsAdditive); + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] += temp[i]; + } + } + } + ++sourcesPos; } - // generate next sources in temporary buffer - // then add or multiply, depending on target flags - absl::Span temp(impl.temp_.data(), numFrames); - while (sourcesPos != sourcesEnd) { - Impl::Source &source = impl.sources_[sourcesPos->first]; - source.gen->generate(source.key, impl.voiceId_, temp); - if (flags & kModIsMultiplicative) { - for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= temp[i]; - } - else if (flags & kModIsPercentMultiplicative) { - for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= 0.01f * temp[i]; - } + // if there were no source, fill output with the neutral element + if (isFirstSource) { + if (targetFlags & kModIsMultiplicative) + sfz::fill(buffer, 1.0f); + else if (targetFlags & kModIsPercentMultiplicative) + sfz::fill(buffer, 100.0f); else { - ASSERT(flags & kModIsAdditive); - for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] += temp[i]; + ASSERT(targetFlags & kModIsAdditive); + sfz::fill(buffer, 0.0f); } - ++sourcesPos; } return buffer.data(); diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index e3381930..7cf51e6f 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -14,6 +14,7 @@ namespace sfz { class ModKey; class ModGenerator; class Voice; +struct Region; /** * @brief Modulation matrix @@ -124,8 +125,9 @@ public: * This clears all the buffers which are per-voice. * * @param voiceId the identifier of the current voice + * @param regionId the identifier of the region of the current voice */ - void beginVoice(NumericId voiceId); + void beginVoice(NumericId voiceId, NumericId regionId); /** * @brief End modulation processing for a given voice. From 4703b939ab3ef6043ea3b92c11e169bab7533ce7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 13:37:08 +0200 Subject: [PATCH 059/445] Only init per-voice modulations which are of the region --- src/sfizz/Voice.cpp | 2 +- src/sfizz/modulations/ModMatrix.cpp | 4 ++-- src/sfizz/modulations/ModMatrix.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 9f1b9197..97fa044d 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -141,7 +141,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate); - resources.modMatrix.initVoice(id); + resources.modMatrix.initVoice(id, region->getId()); } int sfz::Voice::getCurrentSampleQuality() const noexcept diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 74fa25cd..c4893fd0 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -202,13 +202,13 @@ void ModMatrix::init() } } -void ModMatrix::initVoice(NumericId voiceId) +void ModMatrix::initVoice(NumericId voiceId, NumericId regionId) { Impl& impl = *impl_; for (Impl::Source &source : impl.sources_) { const int flags = source.key.flags(); - if (flags & kModIsPerVoice) + if ((flags & kModIsPerVoice) && source.key.region() == regionId) source.gen->init(source.key, voiceId); } } diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 7cf51e6f..4622f4b1 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -104,7 +104,7 @@ public: * @brief Reinitialize modulation source for a given voice. * This must be called first after a voice enters active state. */ - void initVoice(NumericId voiceId); + void initVoice(NumericId voiceId, NumericId regionId); /** * @brief Start modulation processing for the entire cycle. From 0b20932e12507ee43ce570a49c770e0bfa68c30f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 13:52:38 +0200 Subject: [PATCH 060/445] Allow to set a multiplier on source, needed for LFO --- src/sfizz/Region.cpp | 14 +++++++------- src/sfizz/Region.h | 6 +++++- src/sfizz/Synth.cpp | 8 ++++---- src/sfizz/modulations/ModMatrix.cpp | 14 ++++++++------ src/sfizz/modulations/ModMatrix.h | 3 ++- tests/RegionTHelpers.cpp | 4 ++-- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 685d7587..25d38f40 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -940,9 +940,9 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con auto it = std::find_if(connections.begin(), connections.end(), [ccNumber, &target](const Connection& x) -> bool { - return x.first.id() == ModId::Controller && - x.first.parameters().cc == ccNumber && - x.second == target; + return x.source.id() == ModId::Controller && + x.source.parameters().cc == ccNumber && + x.target == target; }); Connection *conn; @@ -951,12 +951,12 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con else { connections.emplace_back(); conn = &connections.back(); - conn->first = ModKey::createCC(ccNumber, 0, 0, 0, 0); - conn->second = target; + conn->source = ModKey::createCC(ccNumber, 0, 0, 0, 0); + conn->target = target; } // - ModKey::Parameters p = conn->first.parameters(); + ModKey::Parameters p = conn->source.parameters(); switch (opcode.category) { case kOpcodeOnCcN: setValueFromOpcode(opcode, p.value, range); @@ -977,7 +977,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con assert(false); break; } - conn->first = ModKey(ModId::Controller, {}, p); + conn->source = ModKey(ModId::Controller, {}, p); } return true; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 0426d6d7..f226a331 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -373,7 +373,11 @@ struct Region { bool triggerOnNote { true }; // Modulation matrix connections - typedef std::pair Connection; + struct Connection { + ModKey source; + ModKey target; + float sourceDepth = 1.0f; + }; std::vector connections; // Parent diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 61fb532d..08d5f5e5 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1357,7 +1357,7 @@ void sfz::Synth::setupModMatrix() for (const Region::Connection& conn : region->connections) { ModGenerator* gen = nullptr; - switch (conn.first.id()) { + switch (conn.source.id()) { case ModId::Controller: gen = genController.get(); break; @@ -1370,8 +1370,8 @@ void sfz::Synth::setupModMatrix() if (!gen) continue; - ModMatrix::SourceId source = mm.registerSource(conn.first, *gen); - ModMatrix::TargetId target = mm.registerTarget(conn.second); + ModMatrix::SourceId source = mm.registerSource(conn.source, *gen); + ModMatrix::TargetId target = mm.registerTarget(conn.target); ASSERT(source); if (!source) { @@ -1385,7 +1385,7 @@ void sfz::Synth::setupModMatrix() continue; } - if (!mm.connect(source, target)) { + if (!mm.connect(source, target, conn.sourceDepth)) { DBG("[sfizz] Failed to connect modulation source and target"); ASSERTFALSE; } diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index c4893fd0..ef68215e 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -34,7 +34,7 @@ struct ModMatrix::Impl { }; struct ConnectionData { - // nothing + float sourceDepth_ {}; }; struct Target { @@ -176,7 +176,7 @@ ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key) return TargetId(it->second); } -bool ModMatrix::connect(SourceId sourceId, TargetId targetId) +bool ModMatrix::connect(SourceId sourceId, TargetId targetId, float sourceDepth) { Impl& impl = *impl_; unsigned sourceIndex = sourceId.number(); @@ -186,7 +186,8 @@ bool ModMatrix::connect(SourceId sourceId, TargetId targetId) return false; Impl::Target& target = impl.targets_[targetIndex]; - /*Impl::ConnectionData& conn =*/ target.connectedSources[sourceIndex]; + Impl::ConnectionData& conn = target.connectedSources[sourceIndex]; + conn.sourceDepth_ = sourceDepth; return true; } @@ -317,6 +318,7 @@ float* ModMatrix::getModulation(TargetId targetId) // then add or multiply, depending on target flags while (sourcesPos != sourcesEnd) { Impl::Source &source = impl.sources_[sourcesPos->first]; + const float sourceDepth = sourcesPos->second.sourceDepth_; const int sourceFlags = source.key.flags(); // only accept per-voice sources of the same region @@ -334,16 +336,16 @@ float* ModMatrix::getModulation(TargetId targetId) source.gen->generate(source.key, impl.voiceId_, temp); if (targetFlags & kModIsMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= temp[i]; + buffer[i] *= sourceDepth * temp[i]; } else if (targetFlags & kModIsPercentMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= 0.01f * temp[i]; + buffer[i] *= (0.01f * sourceDepth) * temp[i]; } else { ASSERT(targetFlags & kModIsAdditive); for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] += temp[i]; + buffer[i] += sourceDepth * temp[i]; } } } diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 4622f4b1..f9338d68 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -90,9 +90,10 @@ public: * * @param sourceId source of the connection * @param targetId target of the connection + * @param sourceDepth amount which multiplies the source output * @return true if the connection was successfully made, otherwise false */ - bool connect(SourceId sourceId, TargetId targetId); + bool connect(SourceId sourceId, TargetId targetId, float sourceDepth); /** * @brief Reinitialize modulation sources overall. diff --git a/tests/RegionTHelpers.cpp b/tests/RegionTHelpers.cpp index d55f05ad..a47b56ed 100644 --- a/tests/RegionTHelpers.cpp +++ b/tests/RegionTHelpers.cpp @@ -27,7 +27,7 @@ sfz::ModKey::Parameters RegionCCView::at(int cc) const { for (const sfz::Region::Connection& conn : region_.connections) { if (match(conn)) { - const sfz::ModKey::Parameters p = conn.first.parameters(); + const sfz::ModKey::Parameters p = conn.source.parameters(); if (p.cc == cc) return p; } @@ -37,5 +37,5 @@ sfz::ModKey::Parameters RegionCCView::at(int cc) const bool RegionCCView::match(const sfz::Region::Connection& conn) const { - return conn.first.id() == sfz::ModId::Controller && conn.second == target_; + return conn.source.id() == sfz::ModId::Controller && conn.target == target_; } From 30788baec410318bca7042a748819690ebbbbeee Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 14:02:25 +0200 Subject: [PATCH 061/445] Add note on NXYZ indices [ci skip] --- src/sfizz/modulations/ModKey.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 48977537..37b8b072 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -57,6 +57,8 @@ public: //! Parameters otherwise, based on the related opcode // eg. `N` in `lfoN`, `N, X` in `lfoN_eqX` struct { uint8_t N, X, Y, Z; }; + // !!! NOTE: NXYZ is expected to be stored in 0-indexed form + // eg. `lfo1_eq2` is N=0, X=1 }; }; From 748b8dc22ce143209d2146ed6e7f76b079b184e7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 15:47:43 +0200 Subject: [PATCH 062/445] Don't forget to apply source depth on first source --- src/sfizz/modulations/ModMatrix.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index ef68215e..b3f5fe13 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -329,6 +329,10 @@ float* ModMatrix::getModulation(TargetId targetId) if (useThisSource) { if (isFirstSource) { source.gen->generate(source.key, impl.voiceId_, buffer); + if (sourceDepth != 1) { + for (uint32_t i = 0; i < numFrames; ++i) + buffer[i] *= sourceDepth; + } isFirstSource = false; } else { From 0e35f392a764e9ab001daca93c4bf7ccc0adde72 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 22:51:44 +0200 Subject: [PATCH 063/445] Fix a mistake in toString for CC --- src/sfizz/modulations/ModKey.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 69d8750f..5ba5e02c 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -85,7 +85,7 @@ std::string ModKey::toString() const case ModId::Controller: return absl::StrCat("Controller ", params_.cc, " {curve=", params_.curve, ", smooth=", params_.smooth, - ", value=", params_.value, ", step=", params_.value, "}"); + ", value=", params_.value, ", step=", params_.step, "}"); case ModId::Envelope: return absl::StrCat("EG ", 1 + params_.N); case ModId::LFO: From 6c870a53d3ee1937217e93692b3455978fb1cf19 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 22:59:59 +0200 Subject: [PATCH 064/445] Add test --- src/sfizz/modulations/ModMatrix.cpp | 42 +++++++++++++++++++++++++++++ src/sfizz/modulations/ModMatrix.h | 5 ++++ tests/ModulationsT.cpp | 22 +++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index b3f5fe13..48b61f25 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -13,6 +13,7 @@ #include "SIMDHelpers.h" #include "Debug.h" #include +#include #include #include @@ -382,4 +383,45 @@ bool ModMatrix::validSource(SourceId id) const return static_cast(id.number()) < impl_->sources_.size(); } +std::string ModMatrix::toDotGraph() const +{ + const Impl& impl = *impl_; + + struct Edge { + std::string source; + std::string target; + }; + + // collect all connections as string pairs + std::vector edges; + for (const Impl::Target& target : impl.targets_) { + for (const auto& cs : target.connectedSources) { + const Impl::Source& source = impl.sources_[cs.first]; + Edge e; + e.source = source.key.toString(); + e.target = target.key.toString(); + edges.push_back(std::move(e)); + } + } + + // alphabetic sort, to produce stable output for unit testing + auto compare = [](const Edge& a, const Edge& b) -> bool { + std::pair aa{a.source, a.target}; + std::pair bb{b.source, b.target}; + return aa < bb; + }; + std::sort(edges.begin(), edges.end(), compare); + + // write dot graph + std::string dot; + dot.reserve(1024); + absl::StrAppend(&dot, "digraph {" "\n"); + for (const Edge& e : edges) { + absl::StrAppend(&dot, "\t" "\"", e.source, "\"" + " -> " "\"", e.target, "\"" "\n"); + } + absl::StrAppend(&dot, "}" "\n"); + return dot; +} + } // namespace sfz diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index f9338d68..516052ae 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -167,6 +167,11 @@ public: */ bool validSource(SourceId id) const; + /** + * @brief Get a representation of the matrix written as a Dot graph. + */ + std::string toDotGraph() const; + private: struct Impl; std::unique_ptr impl_; diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 3db0a96a..b3f6776c 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -6,6 +6,7 @@ #include "sfizz/modulations/ModId.h" #include "sfizz/modulations/ModKey.h" +#include "sfizz/Synth.h" #include "catch2/catch.hpp" TEST_CASE("[Modulations] Identifiers") @@ -74,3 +75,24 @@ TEST_CASE("[Modulations] Display names") REQUIRE(!sfz::ModKey(id).toString().empty()); }); } + +TEST_CASE("[Modulations] Connection graph from SFZ") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + +sample=*sine +amplitude_oncc20=59 amplitude_curvecc20=3 +pitch_oncc42=71 pitch_smoothcc42=32 +pan_oncc36=14.5 pan_stepcc36=1.5 +width_oncc425=29 +)"); + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == R"(digraph { + "Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude" + "Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan" + "Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch" + "Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width" +} +)"); +} From 3a665e503c25e89c9c415db3755283fe41df6455 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 30 Jul 2020 01:20:35 +0200 Subject: [PATCH 065/445] Fix build trouble on MSVC --- src/sfizz/modulations/ModMatrix.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 516052ae..fa1f2633 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -6,6 +6,7 @@ #pragma once #include "../NumericId.h" +#include #include #include From 42c4d964dbfd8dbe32bd9eecdf276fadbd2f0ee0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 30 Jul 2020 13:48:07 +0200 Subject: [PATCH 066/445] Fix small mistakes --- src/sfizz/Synth.cpp | 2 +- tests/ModulationsT.cpp | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 08d5f5e5..71f9f500 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1380,7 +1380,7 @@ void sfz::Synth::setupModMatrix() } ASSERT(target); - if (!source) { + if (!target) { DBG("[sfizz] Failed to register modulation target"); continue; } diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index b3f6776c..709ec36d 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -34,20 +34,22 @@ TEST_CASE("[Modulations] Flags") static auto* checkBasicFlags = +[](int flags) { REQUIRE(flags != sfz::kModFlagsInvalid); - REQUIRE(((flags & sfz::kModIsPerCycle) ^ - (flags & sfz::kModIsPerVoice)) != 0); + REQUIRE((bool(flags & sfz::kModIsPerCycle) + + bool(flags & sfz::kModIsPerVoice)) == 1); }; static auto* checkSourceFlags = +[](int flags) { checkBasicFlags(flags); - // nothing else + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 0); }; static auto* checkTargetFlags = +[](int flags) { checkBasicFlags(flags); - REQUIRE(((flags & sfz::kModIsAdditive) ^ - (flags & sfz::kModIsMultiplicative) ^ - (flags & sfz::kModIsPercentMultiplicative)) != 0); + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 1); }; sfz::ModIds::forEachSourceId([](sfz::ModId id) From acac06aae8e21bc36bc0a6c33718b756b6201041 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 16:26:04 +0200 Subject: [PATCH 067/445] Rename v/r identifier vars to designate them as current --- src/sfizz/modulations/ModMatrix.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 48b61f25..fe5866be 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -24,8 +24,8 @@ struct ModMatrix::Impl { uint32_t samplesPerBlock_ {}; uint32_t numFrames_ {}; - NumericId voiceId_ {}; - NumericId regionId_ {}; + NumericId currentVoiceId_ {}; + NumericId currentRegionId_ {}; struct Source { ModKey key; @@ -249,8 +249,8 @@ void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) { Impl& impl = *impl_; - impl.voiceId_ = voiceId; - impl.regionId_ = regionId; + impl.currentVoiceId_ = voiceId; + impl.currentRegionId_ = regionId; for (Impl::Source &source : impl.sources_) { const int flags = source.key.flags(); @@ -268,8 +268,8 @@ void ModMatrix::endVoice() { Impl& impl = *impl_; const uint32_t numFrames = impl.numFrames_; - const NumericId voiceId = impl.voiceId_; - const NumericId regionId = impl.regionId_; + const NumericId voiceId = impl.currentVoiceId_; + const NumericId regionId = impl.currentRegionId_; for (Impl::Source &source : impl.sources_) { if (!source.bufferReady) { @@ -281,8 +281,8 @@ void ModMatrix::endVoice() } } - impl.voiceId_ = {}; - impl.regionId_ = {}; + impl.currentVoiceId_ = {}; + impl.currentRegionId_ = {}; } float* ModMatrix::getModulation(TargetId targetId) @@ -291,7 +291,7 @@ float* ModMatrix::getModulation(TargetId targetId) return nullptr; Impl& impl = *impl_; - const NumericId regionId = impl.regionId_; + const NumericId regionId = impl.currentRegionId_; const uint32_t targetIndex = targetId.number(); Impl::Target &target = impl.targets_[targetIndex]; const int targetFlags = target.key.flags(); @@ -329,7 +329,7 @@ float* ModMatrix::getModulation(TargetId targetId) if (useThisSource) { if (isFirstSource) { - source.gen->generate(source.key, impl.voiceId_, buffer); + source.gen->generate(source.key, impl.currentVoiceId_, buffer); if (sourceDepth != 1) { for (uint32_t i = 0; i < numFrames; ++i) buffer[i] *= sourceDepth; @@ -338,7 +338,7 @@ float* ModMatrix::getModulation(TargetId targetId) } else { absl::Span temp(impl.temp_.data(), numFrames); - source.gen->generate(source.key, impl.voiceId_, temp); + source.gen->generate(source.key, impl.currentVoiceId_, temp); if (targetFlags & kModIsMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) buffer[i] *= sourceDepth * temp[i]; From fb873ad895444a03b7ae4c1bdadb0b0690b7db1e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 16:28:54 +0200 Subject: [PATCH 068/445] Do not require mod generators to override a couple of methods --- src/sfizz/modulations/ModGenerator.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h index 457460c3..2229ec15 100644 --- a/src/sfizz/modulations/ModGenerator.h +++ b/src/sfizz/modulations/ModGenerator.h @@ -24,12 +24,12 @@ public: /** * @brief Set the sample rate */ - virtual void setSampleRate(double sampleRate) = 0; + virtual void setSampleRate(double sampleRate) { (void)sampleRate; } /** * @brief Set the maximum block size */ - virtual void setSamplesPerBlock(unsigned count) = 0; + virtual void setSamplesPerBlock(unsigned count) { (void)count; } /** * @brief Initialize the generator. From 094107206a33728c3ab53ae5638eb7adcdfecfec Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 17:31:30 +0200 Subject: [PATCH 069/445] Ensure to generate sources once only --- src/sfizz/modulations/ModMatrix.cpp | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index fe5866be..41b1c844 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -51,8 +51,6 @@ struct ModMatrix::Impl { std::vector sources_; std::vector targets_; - - Buffer temp_; }; ModMatrix::ModMatrix() @@ -104,8 +102,6 @@ void ModMatrix::setSamplesPerBlock(unsigned samplesPerBlock) } for (Impl::Target &target : impl.targets_) target.buffer.resize(samplesPerBlock); - - impl.temp_.resize(samplesPerBlock); } ModMatrix::SourceId ModMatrix::registerSource(const ModKey& key, ModGenerator& gen) @@ -315,7 +311,7 @@ float* ModMatrix::getModulation(TargetId targetId) auto sourcesEnd = target.connectedSources.end(); bool isFirstSource = true; - // generate first source in output buffer, next sources in temporary buffer + // generate sources in their dedicated buffers // then add or multiply, depending on target flags while (sourcesPos != sourcesEnd) { Impl::Source &source = impl.sources_[sourcesPos->first]; @@ -328,29 +324,37 @@ float* ModMatrix::getModulation(TargetId targetId) useThisSource = (regionId == source.key.region()); if (useThisSource) { + absl::Span sourceBuffer(source.buffer.data(), numFrames); + + // unless source is already done, process it + if (!source.bufferReady) { + source.gen->generate(source.key, impl.currentVoiceId_, sourceBuffer); + source.bufferReady = true; + } + if (isFirstSource) { - source.gen->generate(source.key, impl.currentVoiceId_, buffer); if (sourceDepth != 1) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= sourceDepth; + buffer[i] = sourceDepth * sourceBuffer[i]; + } + else { + copy(absl::Span(sourceBuffer), buffer); } isFirstSource = false; } else { - absl::Span temp(impl.temp_.data(), numFrames); - source.gen->generate(source.key, impl.currentVoiceId_, temp); if (targetFlags & kModIsMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= sourceDepth * temp[i]; + buffer[i] *= sourceDepth * sourceBuffer[i]; } else if (targetFlags & kModIsPercentMultiplicative) { for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= (0.01f * sourceDepth) * temp[i]; + buffer[i] *= (0.01f * sourceDepth) * sourceBuffer[i]; } else { ASSERT(targetFlags & kModIsAdditive); for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] += sourceDepth * temp[i]; + buffer[i] += sourceDepth * sourceBuffer[i]; } } } @@ -361,12 +365,12 @@ float* ModMatrix::getModulation(TargetId targetId) // if there were no source, fill output with the neutral element if (isFirstSource) { if (targetFlags & kModIsMultiplicative) - sfz::fill(buffer, 1.0f); + fill(buffer, 1.0f); else if (targetFlags & kModIsPercentMultiplicative) - sfz::fill(buffer, 100.0f); + fill(buffer, 100.0f); else { ASSERT(targetFlags & kModIsAdditive); - sfz::fill(buffer, 0.0f); + fill(buffer, 0.0f); } } From eab53ba05ab95d8e29aabbe2c0be54edcf4da1a6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 28 Jul 2020 09:39:05 +0200 Subject: [PATCH 070/445] Add SFZv2 LFO and a small set of modulation targets --- dpf.mk | 4 + src/CMakeLists.txt | 6 + src/sfizz/Config.h | 2 + src/sfizz/Defaults.h | 16 ++ src/sfizz/LFO.cpp | 295 ++++++++++++++++++++++++++ src/sfizz/LFO.h | 112 ++++++++++ src/sfizz/LFODescription.cpp | 31 +++ src/sfizz/LFODescription.h | 48 +++++ src/sfizz/Region.cpp | 241 +++++++++++++++++++++ src/sfizz/Region.h | 5 + src/sfizz/Synth.cpp | 9 + src/sfizz/Synth.h | 3 + src/sfizz/Voice.cpp | 19 ++ src/sfizz/Voice.h | 19 +- src/sfizz/modulations/sources/LFO.cpp | 67 ++++++ src/sfizz/modulations/sources/LFO.h | 23 ++ 16 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 src/sfizz/LFO.cpp create mode 100644 src/sfizz/LFO.h create mode 100644 src/sfizz/LFODescription.cpp create mode 100644 src/sfizz/LFODescription.h create mode 100644 src/sfizz/modulations/sources/LFO.cpp create mode 100644 src/sfizz/modulations/sources/LFO.h diff --git a/dpf.mk b/dpf.mk index 4060db8d..ec3038fd 100644 --- a/dpf.mk +++ b/dpf.mk @@ -1,3 +1,4 @@ + # # A build file to help using sfizz with the DISTRHO Plugin Framework (DPF) # ------------------------------------------------------------------------ @@ -65,6 +66,7 @@ SFIZZ_SOURCES = \ src/sfizz/modulations/ModKeyHash.cpp \ src/sfizz/modulations/ModMatrix.cpp \ src/sfizz/modulations/sources/Controller.cpp \ + src/sfizz/modulations/sources/LFO.cpp \ src/sfizz/effects/Compressor.cpp \ src/sfizz/effects/Disto.cpp \ src/sfizz/effects/Eq.cpp \ @@ -91,6 +93,8 @@ SFIZZ_SOURCES = \ src/sfizz/FilterPool.cpp \ src/sfizz/FloatEnvelopes.cpp \ src/sfizz/Logger.cpp \ + src/sfizz/LFO.cpp \ + src/sfizz/LFODescription.cpp \ src/sfizz/MidiState.cpp \ src/sfizz/OpcodeCleanup.cpp \ src/sfizz/Opcode.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a1199157..63cdd25f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ set (SFIZZ_HEADERS sfizz/modulations/ModMatrix.h sfizz/modulations/ModGenerator.h sfizz/modulations/sources/Controller.h + sfizz/modulations/sources/LFO.h sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArraySSE.h @@ -70,6 +71,8 @@ set (SFIZZ_HEADERS sfizz/Interpolators.h sfizz/Interpolators.hpp sfizz/Logger.h + sfizz/LFO.h + sfizz/LFODescription.h sfizz/MathHelpers.h sfizz/MidiState.h sfizz/ModifierHelpers.h @@ -133,11 +136,14 @@ set (SFIZZ_SOURCES sfizz/RTSemaphore.cpp sfizz/Panning.cpp sfizz/Effects.cpp + sfizz/LFO.cpp + sfizz/LFODescription.cpp sfizz/modulations/ModId.cpp sfizz/modulations/ModKey.cpp sfizz/modulations/ModKeyHash.cpp sfizz/modulations/ModMatrix.cpp sfizz/modulations/sources/Controller.cpp + sfizz/modulations/sources/LFO.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 03f7f378..d3592bc5 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -64,6 +64,8 @@ namespace config { constexpr unsigned int defaultAlignment { 16 }; constexpr int filtersInPool { maxVoices * 2 }; constexpr int excessFileFrames { 8 }; + constexpr int maxLFOSubs { 8 }; + constexpr int maxLFOSteps { 128 }; /** * @brief The threshold for age stealing. * In percentage of the voice's max age. diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 8be2fc96..c30d4192 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -207,6 +207,22 @@ namespace Default constexpr int bendStep { 1 }; constexpr uint8_t bendSmooth { 0 }; + // Modulation: LFO + constexpr int numLFOs { 4 }; + constexpr int numLFOSubs { 2 }; + constexpr int numLFOSteps { 8 }; + constexpr Range lfoFreqRange { 0.0, 100.0 }; + constexpr Range lfoPhaseRange { 0.0, 360.0 }; + constexpr Range lfoDelayRange { 0.0, 30.0 }; + constexpr Range lfoFadeRange { 0.0, 30.0 }; + constexpr Range lfoCountRange { 0, 1000 }; + constexpr Range lfoStepsRange { 0, static_cast(config::maxLFOSteps) }; + constexpr Range lfoStepXRange { -100.0, 100.0 }; + constexpr Range lfoWaveRange { 0, 15 }; + constexpr Range lfoOffsetRange { -1.0, 1.0 }; + constexpr Range lfoRatioRange { 0.0, 100.0 }; + constexpr Range lfoScaleRange { 0.0, 1.0 }; + // Envelope generators constexpr float attack { 0 }; constexpr float decay { 0 }; diff --git a/src/sfizz/LFO.cpp b/src/sfizz/LFO.cpp new file mode 100644 index 00000000..6d1da07d --- /dev/null +++ b/src/sfizz/LFO.cpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "LFO.h" +#include "LFODescription.h" +#include "MathHelpers.h" +#include "SIMDHelpers.h" +#include "Config.h" +#include +#include +#include + +namespace sfz { + +struct LFO::Impl { + float sampleRate_ = 0; + + // control + const LFODescription* desc_ = nullptr; + + // state + size_t delayFramesLeft_ = 0; + float fadeInPole_ = 0; + float fadeInMemory_ = 0; + std::array subPhases_ {{}}; + std::array sampleHoldMem_ {{}}; +}; + +LFO::LFO() + : impl_(new Impl) +{ + impl_->sampleRate_ = config::defaultSampleRate; + impl_->desc_ = &LFODescription::getDefault(); +} + +LFO::~LFO() +{ +} + +void LFO::setSampleRate(double sampleRate) +{ + impl_->sampleRate_ = sampleRate; +} + +void LFO::configure(const LFODescription* desc) +{ + impl_->desc_ = desc ? desc : &LFODescription::getDefault(); +} + +void LFO::start() +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const float sampleRate = impl.sampleRate_; + + impl.subPhases_.fill(desc.phase0); + impl.sampleHoldMem_.fill(0.0f); + + const float delay = desc.delay; + impl.delayFramesLeft_ = (delay > 0) ? static_cast(std::ceil(sampleRate * delay)) : 0u; + + const float fade = desc.fade; + impl.fadeInPole_ = (fade > 0) ? std::exp(-1.0 / (fade * sampleRate)) : 0.0f; + impl.fadeInMemory_ = 0; +} + +template <> +inline float LFO::eval(float phase) +{ + float y = -4 * phase + 2; + y = (phase < 0.25f) ? (4 * phase) : y; + y = (phase > 0.75f) ? (4 * phase - 4) : y; + return y; +} + +template <> +inline float LFO::eval(float phase) +{ + float x = phase + phase - 1; + return 4 * x * (1 - std::fabs(x)); +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.75f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.5f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.25f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return (phase < 0.125f) ? +1.0f : -1.0f; +} + +template <> +inline float LFO::eval(float phase) +{ + return 2 * phase - 1; +} + +template <> +inline float LFO::eval(float phase) +{ + return 1 - 2 * phase; +} + +template +void LFO::processWave(unsigned nth, absl::Span out) +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const LFODescription::Sub& sub = desc.sub[nth]; + const size_t numFrames = out.size(); + + float samplePeriod = 1.0f / impl.sampleRate_; + float phase = impl.subPhases_[nth]; + float baseFreq = desc.freq; + float offset = sub.offset; + float ratio = sub.ratio; + float scale = sub.scale; + + for (size_t i = 0; i < numFrames; ++i) { + out[i] += offset + scale * eval(phase); + + // TODO(jpc) lfoN_count: number of repetitions + + float incrPhase = ratio * samplePeriod * baseFreq; + phase += incrPhase; + int numWraps = (int)phase; + phase -= numWraps; + } + + impl.subPhases_[nth] = phase; +} + +template +void LFO::processSH(unsigned nth, absl::Span out) +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const LFODescription::Sub& sub = desc.sub[nth]; + const size_t numFrames = out.size(); + + float samplePeriod = 1.0f / impl.sampleRate_; + float phase = impl.subPhases_[nth]; + float baseFreq = desc.freq; + float offset = sub.offset; + float ratio = sub.ratio; + float scale = sub.scale; + float sampleHoldValue = impl.sampleHoldMem_[nth]; + + for (size_t i = 0; i < numFrames; ++i) { + out[i] += offset + scale * sampleHoldValue; + + // TODO(jpc) lfoN_count: number of repetitions + + float incrPhase = ratio * samplePeriod * baseFreq; + + // value updates twice every period + bool updateValue = (int)(phase * 2.0) != (int)((phase + incrPhase) * 2.0); + + phase += incrPhase; + int numWraps = (int)phase; + phase -= numWraps; + + if (updateValue) { + std::uniform_real_distribution dist(-1.0f, +1.0f); + sampleHoldValue = dist(Random::randomGenerator); + } + } + + impl.subPhases_[nth] = phase; + impl.sampleHoldMem_[nth] = sampleHoldValue; +} + +void LFO::processSteps(absl::Span out) +{ + unsigned nth = 0; + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const LFODescription::Sub& sub = desc.sub[nth]; + const size_t numFrames = out.size(); + + const LFODescription::StepSequence& seq = *desc.seq; + const float* steps = seq.steps.data(); + unsigned numSteps = seq.steps.size(); + + if (numSteps <= 0) + return; + + float samplePeriod = 1.0f / impl.sampleRate_; + float phase = impl.subPhases_[nth]; + float baseFreq = desc.freq; + float offset = sub.offset; + float ratio = sub.ratio; + float scale = sub.scale; + + for (size_t i = 0; i < numFrames; ++i) { + float step = steps[static_cast(phase * numSteps)]; + out[i] += offset + scale * step; + + // TODO(jpc) lfoN_count: number of repetitions + + float incrPhase = ratio * samplePeriod * baseFreq; + phase += incrPhase; + int numWraps = (int)phase; + phase -= numWraps; + } + + impl.subPhases_[nth] = phase; +} + +void LFO::process(absl::Span out) +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + size_t numFrames = out.size(); + + fill(out, 0.0f); + + size_t skipFrames = std::min(numFrames, impl.delayFramesLeft_); + if (skipFrames > 0) { + impl.delayFramesLeft_ -= skipFrames; + out.remove_prefix(skipFrames); + numFrames -= skipFrames; + } + + unsigned subno = 0; + const unsigned countSubs = desc.sub.size(); + + if (countSubs < 1) + return; + + if (desc.seq) { + processSteps(out); + ++subno; + } + + for (; subno < countSubs; ++subno) { + switch (desc.sub[subno].wave) { + case LFOWave::Triangle: + processWave(subno, out); + break; + case LFOWave::Sine: + processWave(subno, out); + break; + case LFOWave::Pulse75: + processWave(subno, out); + break; + case LFOWave::Square: + processWave(subno, out); + break; + case LFOWave::Pulse25: + processWave(subno, out); + break; + case LFOWave::Pulse12_5: + processWave(subno, out); + break; + case LFOWave::Ramp: + processWave(subno, out); + break; + case LFOWave::Saw: + processWave(subno, out); + break; + case LFOWave::RandomSH: + processSH(subno, out); + break; + } + } + + float fadeIn = impl.fadeInMemory_; + const float fadeInPole = impl.fadeInPole_; + for (size_t i = 0; i < numFrames; ++i) { + out[i] *= fadeIn; + fadeIn = fadeInPole * fadeIn + (1 - fadeInPole); + } + impl.fadeInMemory_ = fadeIn; +} + +} // namespace sfz diff --git a/src/sfizz/LFO.h b/src/sfizz/LFO.h new file mode 100644 index 00000000..86d1f0f3 --- /dev/null +++ b/src/sfizz/LFO.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +namespace sfz { + +enum class LFOWave : int; +struct LFODescription; + +/* + * General + + lfoN_freq: Base frequency - Allow modulations at A-rate + lfoN_phase: Initial phase + lfoN_delay: Delay + lfoN_fade: Time to fade-in + lfoN_count: Number of repetitions - not implemented in ARIA + lfoN_steps: Length of the step sequence - 1 to 128 + lfoN_steps_onccX: ??? TODO(jpc) seen in Rapture + lfoN_stepX: Value of the Xth step of the sequence - -100% to +100% + lfoN_stepX_onccY: ??? TODO(jpc) check this. override/modulate step in sequence? + + note: LFO evaluates between -1 to +1 + + note: make the step sequencer override the main wave when present. + subwaves are ARIA, step sequencer is Cakewalk, so do our own thing + which makes the most sense. + + * Subwaveforms + X: - #1/omitted: the main wave + - #2-#8: a subwave + + note: if there are gaps in subwaveforms, these subwaveforms which are gaps + will be initialized and processed. + + example: lfo1_ratio4=1.0 // instanciate implicitly the subs #2 and #3 + + lfoN_wave[X]: Wave + lfoN_offset[X]: DC offset - Add to LFO output; not affected by scale. + lfoN_ratio[X]: Sub ratio - Frequency = (Ratio * Base Frequency) + lfoN_scale[X]: Sub scale - Amplitude of sub +*/ + +class LFO { +public: + LFO(); + ~LFO(); + + /** + Sets the sample rate. + */ + void setSampleRate(double sampleRate); + + /** + Attach some control parameters to this LFO. + The control structure is owned by the caller. + */ + void configure(const LFODescription* desc); + + /** + Start processing a LFO as a region is triggered. + Prepares the delay, phases, fade-in, etc.. + */ + void start(); + + /** + Process a cycle of the oscillator. + + TODO(jpc) frequency modulations + */ + void process(absl::Span out); + +private: + /** + Evaluate the wave at a given phase. + Phase must be in the range 0 to 1 excluded. + */ + template + static float eval(float phase); + + /** + Process the nth subwaveform, adding to the buffer. + + This definition is duplicated per each wave, a strategy to avoid a switch + on wave type inside the frame loop. + */ + template + void processWave(unsigned nth, absl::Span out); + + /** + Process a sample-and-hold subwaveform, adding to the buffer. + */ + template + void processSH(unsigned nth, absl::Span out); + + /** + Process the step sequencer, adding to the buffer. + */ + void processSteps(absl::Span out); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/LFODescription.cpp b/src/sfizz/LFODescription.cpp new file mode 100644 index 00000000..4e846ef2 --- /dev/null +++ b/src/sfizz/LFODescription.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "LFODescription.h" + +namespace sfz { + +LFODescription::LFODescription() +{ + sub.resize(1); +} + +LFODescription::~LFODescription() +{ +} + +const LFODescription& LFODescription::getDefault() +{ + static LFODescription desc = []() -> LFODescription + { + LFODescription desc; + desc.sub.resize(1); + return desc; + }(); + return desc; +} + +} // namespace sfz diff --git a/src/sfizz/LFODescription.h b/src/sfizz/LFODescription.h new file mode 100644 index 00000000..8f5344c9 --- /dev/null +++ b/src/sfizz/LFODescription.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +namespace sfz { + +enum class LFOWave : int { + Triangle, + Sine, + Pulse75, + Square, + Pulse25, + Pulse12_5, + Ramp, + Saw, + // ARIA extra + RandomSH = 12, +}; + +struct LFODescription { + LFODescription(); + ~LFODescription(); + static const LFODescription& getDefault(); + float freq = 0; // lfoN_freq + float phase0 = 0; // lfoN_phase + float delay = 0; // lfoN_delay + float fade = 0; // lfoN_fade + unsigned count = 0; // lfoN_count + struct Sub { + LFOWave wave = LFOWave::Triangle; // lfoN_wave[X] + float offset = 0; // lfoN_offset[X] + float ratio = 1; // lfoN_ratio[X] + float scale = 1; // lfoN_scale[X] + }; + struct StepSequence { + std::vector steps {}; // lfoN_stepX - normalized to unity + }; + absl::optional seq; + std::vector sub; +}; + +} // namespace sfz diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 25d38f40..95619178 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -786,6 +786,228 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, bendSmooth, Default::smoothCCRange); break; + // Modulation: LFO + case hash("lfo&_freq"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].freq, Default::lfoFreqRange); + } + break; + case hash("lfo&_phase"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoPhaseRange)) { + float normalPhase = *value * (1.0 / 360.0); + normalPhase -= int(normalPhase); + normalPhase += (normalPhase < 0) ? 1 : 0; + lfos[lfoNumber - 1].phase0 = normalPhase; + } + } + break; + case hash("lfo&_delay"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].delay, Default::lfoDelayRange); + } + break; + case hash("lfo&_fade"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].fade, Default::lfoFadeRange); + } + break; + case hash("lfo&_count"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + setValueFromOpcode(opcode, lfos[lfoNumber - 1].count, Default::lfoCountRange); + } + break; + case hash("lfo&_steps"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoStepsRange)) { + if (!lfos[lfoNumber - 1].seq) + lfos[lfoNumber - 1].seq = LFODescription::StepSequence(); + lfos[lfoNumber - 1].seq->steps.resize(*value); + } + } + break; + case hash("lfo&_step&"): + { + const auto lfoNumber = opcode.parameters.front(); + const auto stepNumber = opcode.parameters[1]; + if (lfoNumber == 0 || stepNumber == 0 || stepNumber > config::maxLFOSteps) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoStepXRange)) { + if (!lfos[lfoNumber - 1].seq) + lfos[lfoNumber - 1].seq = LFODescription::StepSequence(); + if (!extendIfNecessary(lfos[lfoNumber - 1].seq->steps, stepNumber, Default::numLFOSteps)) + return false; + lfos[lfoNumber - 1].seq->steps[stepNumber - 1] = *value * 0.01f; + } + } + break; + case hash("lfo&_wave&"): // also lfo&_wave + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoWaveRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].wave = static_cast(*value); + } + } + break; + case hash("lfo&_offset&"): // also lfo&_offset + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoOffsetRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].offset = *value; + } + } + break; + case hash("lfo&_ratio&"): // also lfo&_ratio + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoRatioRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].ratio = *value; + } + } + break; + case hash("lfo&_scale&"): // also lfo&_scale + { + const auto lfoNumber = opcode.parameters.front(); + const auto subNumber = opcode.parameters[1]; + if (lfoNumber == 0 || subNumber == 0 || subNumber > config::maxLFOSubs) + return false; + if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) + return false; + if (auto value = readOpcode(opcode.value, Default::lfoScaleRange)) { + if (!extendIfNecessary(lfos[lfoNumber - 1].sub, subNumber, Default::numLFOSubs)) + return false; + lfos[lfoNumber - 1].sub[subNumber - 1].scale = *value; + } + } + break; + + // Modulation: LFO (targets) + case hash("lfo&_amplitude"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Amplitude, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_pan"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::panCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Pan, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_width"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::widthCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Width, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_position"): // sfizz extension + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::positionCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Position, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_pitch"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::tuneCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Pitch, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("lfo&_volume"): + { + const auto lfoNumber = opcode.parameters.front(); + if (lfoNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::volumeCCRange)) { + ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + ModKey target = ModKey::createNXYZ(ModId::Volume, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + // Amplitude Envelope case hash("ampeg_attack"): setValueFromOpcode(opcode, amplitudeEG.attack, Default::egTimeRange); @@ -1324,3 +1546,22 @@ float sfz::Region::getBendInCents(float bend) const noexcept { return bend > 0.0f ? bend * static_cast(bendUp) : -bend * static_cast(bendDown); } + +sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source, const ModKey& target) +{ + auto pred = [&source, &target](const Connection& c) + { + return c.source == source && c.target == target; + }; + + auto it = std::find_if(connections.begin(), connections.end(), pred); + if (it != connections.end()) + return *it; + + sfz::Region::Connection c; + c.source = source; + c.target = target; + + connections.push_back(c); + return connections.back(); +} diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index f226a331..a4c4f291 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -12,6 +12,7 @@ #include "EGDescription.h" #include "EQDescription.h" #include "FilterDescription.h" +#include "LFODescription.h" #include "Opcode.h" #include "AudioBuffer.h" #include "MidiState.h" @@ -364,6 +365,9 @@ struct Region { EGDescription pitchEG; EGDescription filterEG; + // LFOs + std::vector lfos; + bool hasStereoSample { false }; // Effects @@ -379,6 +383,7 @@ struct Region { float sourceDepth = 1.0f; }; std::vector connections; + Connection& getOrCreateConnection(const ModKey& source, const ModKey& target); // Parent RegionSet* parent { nullptr }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 71f9f500..8a268b64 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -16,6 +16,7 @@ #include "modulations/ModKey.h" #include "modulations/ModId.h" #include "modulations/sources/Controller.h" +#include "modulations/sources/LFO.h" #include "pugixml.hpp" #include "absl/algorithm/container.h" #include "absl/memory/memory.h" @@ -42,6 +43,7 @@ sfz::Synth::Synth(int numVoices) // modulation sources genController.reset(new ControllerSource(resources)); + genLFO.reset(new LFOSource(*this)); } sfz::Synth::~Synth() @@ -452,6 +454,7 @@ void sfz::Synth::finalizeSfzLoad() size_t maxFilters { 0 }; size_t maxEQs { 0 }; + size_t maxLFOs { 0 }; while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); @@ -562,6 +565,7 @@ void sfz::Synth::finalizeSfzLoad() region->registerTempo(2.0f); maxFilters = max(maxFilters, region->filters.size()); maxEQs = max(maxEQs, region->equalizers.size()); + maxLFOs = max(maxLFOs, region->lfos.size()); ++currentRegionIndex; } @@ -571,6 +575,7 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; + settingsPerVoice.maxLFOs = maxLFOs; applySettingsPerVoice(); @@ -1346,6 +1351,7 @@ void sfz::Synth::applySettingsPerVoice() for (auto& voice : voices) { voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); + voice->setMaxLFOsPerVoice(settingsPerVoice.maxLFOs); } } @@ -1361,6 +1367,9 @@ void sfz::Synth::setupModMatrix() case ModId::Controller: gen = genController.get(); break; + case ModId::LFO: + gen = genLFO.get(); + break; default: DBG("[sfizz] Have unknown type of source generator"); break; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 8133b676..5b49ddd1 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -27,6 +27,7 @@ namespace sfz { class ControllerSource; +class LFOSource; /** * @brief This class is the core of the sfizz library. In C++ it is the main point @@ -767,11 +768,13 @@ private: // Modulation source generators std::unique_ptr genController; + std::unique_ptr genLFO; // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; size_t maxEQs { 0 }; + size_t maxLFOs { 0 }; }; SettingsPerVoice settingsPerVoice; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 97fa044d..03194ece 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -12,6 +12,7 @@ #include "SIMDHelpers.h" #include "Panning.h" #include "SfzHelpers.h" +#include "LFO.h" #include "modulations/ModId.h" #include "modulations/ModKey.h" #include "modulations/ModMatrix.h" @@ -34,6 +35,10 @@ sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources) filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); } +sfz::Voice::~Voice() +{ +} + void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept { ASSERT(value >= 0.0f && value <= 1.0f); @@ -235,6 +240,9 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept for (WavetableOscillator& osc : waveOscillators) osc.init(sampleRate); + + for (auto& lfo : lfos) + lfo->setSampleRate(sampleRate); } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept @@ -773,6 +781,17 @@ void sfz::Voice::setMaxEQsPerVoice(size_t numFilters) equalizers.reserve(numFilters); } +void sfz::Voice::setMaxLFOsPerVoice(size_t numLFOs) +{ + lfos.resize(numLFOs); + + for (size_t i = 0; i < numLFOs; ++i) { + auto lfo = absl::make_unique(); + lfo->setSampleRate(sampleRate); + lfos[i] = std::move(lfo); + } +} + void sfz::Voice::setupOscillatorUnison() { int m = region->oscillatorMulti; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 52a7f0c8..17913bb3 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -22,6 +22,7 @@ namespace sfz { enum InterpolatorModel : int; +class LFO; /** * @brief The SFZ voice are the polyphony holders. They get activated by the synth * and tasked to play a given region until the end, stopping on note-offs, off-groups @@ -38,6 +39,9 @@ public: * @param midiState */ Voice(int voiceNumber, Resources& resources); + + ~Voice(); + enum class TriggerType { NoteOn, NoteOff, @@ -270,6 +274,12 @@ public: * @return */ const Region* getRegion() const noexcept { return region; } + /** + * @brief Get the LFO designated by the given index + * + * @param index + */ + LFO* getLFO(size_t index) { return lfos[index].get(); } /** * @brief Set the max number of filters per voice * @@ -279,9 +289,15 @@ public: /** * @brief Set the max number of EQs per voice * - * @param numFilters + * @param numEQs */ void setMaxEQsPerVoice(size_t numEQs); + /** + * @brief Set the max number of LFOs per voice + * + * @param numLFOs + */ + void setMaxLFOsPerVoice(size_t numLFOs); /** * @brief Release the voice after a given delay * @@ -433,6 +449,7 @@ private: std::vector filters; std::vector equalizers; + std::vector> lfos; ADSREnvelope egEnvelope; float bendStepFactor { centsFactor(1) }; diff --git a/src/sfizz/modulations/sources/LFO.cpp b/src/sfizz/modulations/sources/LFO.cpp new file mode 100644 index 00000000..0f604898 --- /dev/null +++ b/src/sfizz/modulations/sources/LFO.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "LFO.h" +#include "../../LFO.h" +#include "../../Synth.h" +#include "../../Voice.h" +#include "../../SIMDHelpers.h" +#include "../../Config.h" +#include "../../Debug.h" + +namespace sfz { + +LFOSource::LFOSource(Synth &synth) + : synth_(&synth) +{ +} + +void LFOSource::init(const ModKey& sourceKey, NumericId voiceId) +{ + Synth& synth = *synth_; + unsigned lfoIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (lfoIndex >= region->lfos.size()) { + ASSERTFALSE; + return; + } + + LFO* lfo = voice->getLFO(lfoIndex); + lfo->configure(®ion->lfos[lfoIndex]); + lfo->start(); +} + +void LFOSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + Synth& synth = *synth_; + const unsigned lfoIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + fill(buffer, 0.0f); + return; + } + + const Region* region = voice->getRegion(); + if (lfoIndex >= region->lfos.size()) { + ASSERTFALSE; + fill(buffer, 0.0f); + return; + } + + LFO* lfo = voice->getLFO(lfoIndex); + lfo->process(buffer); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/LFO.h b/src/sfizz/modulations/sources/LFO.h new file mode 100644 index 00000000..83a1e30f --- /dev/null +++ b/src/sfizz/modulations/sources/LFO.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../ModGenerator.h" + +namespace sfz { +class Synth; + +class LFOSource : public ModGenerator { +public: + explicit LFOSource(Synth &synth); + void init(const ModKey& sourceKey, NumericId voiceId) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + Synth* synth_ = nullptr; +}; + +} // namespace sfz From 0d316399db4cc814ba529a156aa75713c668599f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 5 Aug 2020 01:00:01 +0200 Subject: [PATCH 071/445] Add the LFO plotting tool --- tests/CMakeLists.txt | 3 + tests/PlotLFO.cpp | 100 ++++++++++++++++++++++++++++++++ tests/TestFiles/lfo_subwave.sfz | 19 ++++++ 3 files changed, 122 insertions(+) create mode 100644 tests/PlotLFO.cpp create mode 100644 tests/TestFiles/lfo_subwave.sfz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4018edee..9abe75de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -94,6 +94,9 @@ target_link_libraries(sfizz_plot_curve PRIVATE sfizz::sfizz) add_executable(sfizz_plot_wavetables PlotWavetables.cpp) target_link_libraries(sfizz_plot_wavetables PRIVATE sfizz::sfizz) +add_executable(sfizz_plot_lfo PlotLFO.cpp) +target_link_libraries(sfizz_plot_lfo PRIVATE sfizz::sfizz) + add_executable(sfizz_file_instrument FileInstrument.cpp) target_link_libraries(sfizz_file_instrument PRIVATE sfizz::sfizz) diff --git a/tests/PlotLFO.cpp b/tests/PlotLFO.cpp new file mode 100644 index 00000000..c8f63e2f --- /dev/null +++ b/tests/PlotLFO.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +/** + This program generates the data file of a LFO output recorded for a fixed + duration. The file contains columns for each LFO in the SFZ region. + The columns are: Time, Lfo1, ... LfoN + One can use Gnuplot to display this data. + Example: + sfizz_plot_lfo file.sfz > lfo.dat + gnuplot + plot "lfo.dat" using 1:2 with lines + */ + +#include "sfizz/Synth.h" +#include "sfizz/LFO.h" +#include "sfizz/LFODescription.h" +#include +#include +#include +#include + +//============================================================================== + +static constexpr double sampleRate = 44100.0; // sample rate used to compute +static constexpr double duration = 5.0; // length in seconds + +/** + Print usage information + */ +static void usage() +{ + std::cerr << "Usage: sfizz_plot_lfo " "\n"; +} + +static std::vector lfoDescriptionFromSfzFile(const fs::path &sfzPath, bool &success) +{ + sfz::Synth synth; + + if (!synth.loadSfzFile(sfzPath)) { + std::cerr << "Cannot load the SFZ file.\n"; + success = false; + return {}; + } + + if (synth.getNumRegions() != 1) { + std::cerr << "The SFZ file must contain exactly one region.\n"; + success = false; + return {}; + } + + success = true; + return synth.getRegionView(0)->lfos; +} + +/** + Program which loads LFO configuration and generates plot data for the given duration. + */ +int main(int argc, char* argv[]) +{ + if (argc < 2 || argc > 2) { + usage(); + return 1; + } + + fs::path sfzPath = argv[1]; + bool success = false; + const std::vector desc = lfoDescriptionFromSfzFile(sfzPath, success); + if (!success) + return 1; + + size_t numLfos = desc.size(); + std::vector lfos(numLfos); + + for (size_t l = 0; l < numLfos; ++l) { + lfos[l].setSampleRate(sampleRate); + lfos[l].configure(&desc[l]); + } + + size_t numFrames = (size_t)std::ceil(sampleRate * duration); + std::vector outputMemory(numLfos * numFrames); + + std::vector> lfoOutputs(numLfos); + for (size_t l = 0; l < numLfos; ++l) { + lfoOutputs[l] = absl::MakeSpan(&outputMemory[l * numFrames], numFrames); + lfos[l].process(lfoOutputs[l]); + } + + for (size_t i = 0; i < numFrames; ++i) { + std::cout << (i / sampleRate); + for (size_t l = 0; l < numLfos; ++l) + std::cout << ' ' << lfoOutputs[l][i]; + std::cout << '\n'; + } + + return 0; +} diff --git a/tests/TestFiles/lfo_subwave.sfz b/tests/TestFiles/lfo_subwave.sfz new file mode 100644 index 00000000..80867c3d --- /dev/null +++ b/tests/TestFiles/lfo_subwave.sfz @@ -0,0 +1,19 @@ + +sample=*noise +lokey=0 +hikey=127 +cutoff=1000.0 +fil_type=brf_2p +lfo1_cutoff=1200.0 +// +lfo1_freq=1 +lfo1_phase=0.5 +lfo1_wave=3 +lfo1_delay=0.5 +lfo1_fade=0.5 +lfo1_wave2=1 +lfo1_offset2=0.2 +lfo1_ratio2=0.7 +lfo1_scale2=0.3 +// +lfo2_freq=0.5 From 1b5a841d97c8ed083985ec36ee2c434967408af3 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 22:57:28 +0200 Subject: [PATCH 072/445] Handle default switch at all levels --- src/sfizz/Synth.cpp | 6 ++++++ tests/SynthT.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 606d3497..c1983016 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -226,6 +226,9 @@ void sfz::Synth::handleMasterOpcodes(const std::vector& members) if (auto value = readOpcode(member.value, Default::polyphonyRange)) currentSet->setPolyphonyLimit(*value); break; + case hash("sw_default"): + setValueFromOpcode(member, defaultSwitch, Default::keyRange); + break; } } } @@ -267,6 +270,9 @@ void sfz::Synth::handleGroupOpcodes(const std::vector& members, const st case hash("polyphony"): setValueFromOpcode(member, maxPolyphony, Default::polyphonyRange); break; + case hash("sw_default"): + setValueFromOpcode(member, defaultSwitch, Default::keyRange); + break; } }; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index a71cb91d..ef63db86 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -939,3 +939,45 @@ TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the a REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); } + +TEST_CASE("[Synth] sw_default works at a global level") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sw_default=36 sw_lokey=36 sw_hikey=39 + sw_last=36 key=62 sample=*sine + sw_last=37 key=63 sample=*sine + )"); + synth.noteOn(0, 63, 85); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); +} + +TEST_CASE("[Synth] sw_default works at a master level") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sw_default=36 sw_lokey=36 sw_hikey=39 + sw_last=36 key=62 sample=*sine + sw_last=37 key=63 sample=*sine + )"); + synth.noteOn(0, 63, 85); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); +} + +TEST_CASE("[Synth] sw_default works at a group level") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sw_default=36 sw_lokey=36 sw_hikey=39 + sw_last=36 key=62 sample=*sine + sw_last=37 key=63 sample=*sine + )"); + synth.noteOn(0, 63, 85); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); +} From 2eb12daf0cb41789c23ebbb69741131c2a038c39 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 9 Aug 2020 22:36:57 +0200 Subject: [PATCH 073/445] Add a test for surge and clm wavetable files --- tests/TestFiles/wavetables/clm.wav | Bin 0 -> 2152 bytes tests/TestFiles/wavetables/surge.wav | Bin 0 -> 1080 bytes tests/WavetablesT.cpp | 32 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 tests/TestFiles/wavetables/clm.wav create mode 100644 tests/TestFiles/wavetables/surge.wav diff --git a/tests/TestFiles/wavetables/clm.wav b/tests/TestFiles/wavetables/clm.wav new file mode 100644 index 0000000000000000000000000000000000000000..07f44c4230d8da4269a8d0bfba400798c132a34e GIT binary patch literal 2152 zcmXBUVNBF@9LMorfOeA64ahNO?qY^Sjt)|W#{GY%A)ZbhsUtFDha%n#kmMpllH0V6 z2d=Tjnikcqxn+$tY(8KG|KF`}QDYM~9x(F(53IRziJm~#FwwM3i@&Et+-|y$6 z?%!2k9}gJw%C^QGZ5_!_zA+}-JYwobjR{`KGC3y1H*GCPlQ~>XmNBg@$ri&C`Wegp zsW$vyEVAOu4AahrM4vN_%tt zqh5L4xcB6rKX^Z%zV4;&-1Odh;WzL1Lw|W2USIMeBiZ)je**UAmONXYD6pA-u>Ct{v)!K9V*mX*V!P(I+3edpZ2pa1HtVy!HnR6+TlvdD zdmz?ii-%vcEBt1A;aZD*^{ZAp|Hg6q#i|pw`E0xWbiUIDR=sI=7Ixdm9whCf-}c!1 z2YT%%KfYuA!qc|w#eO@w^K%0sy*ALXRHl$-KX zj>=QHDqrQSyp_B1R}bn#y{I4cq`uUf`cseUQ@yHR^{l?tdsXG*&v2dK^CaI-^7~2d zo8*3<=X{>`ImhQbpL2cA_j!-c`+VN(^M0Q>eCF|)%V$2HIeq4J%+H$6k*8 z9D6$Ub?oif-;skO4@WMJd>lDB@^a+n$j_0ZBTq-Jj(i3N-_^K`Dx*L(Cny;two z9GXXSX+F)Vc{R7@*B;tOduc!IseQG#_E!$dL%Ap)<)plnoAOhR%2T;2U*)X4mAmp+ z59&j`s2}yDzSNugQ;+IXy{cdJtiB)q>tFt}mK>0Qi5L??6mg_5gfzyG!7LWA2;&(O zKoCU;p$uWvAc`0o(1bV==t3`2IEO);#}I~b0V7D`5-wvDW4MZOOdx|POk)PKn8Q47 zVF9;s2Y0cEd$^A!82%5)LJn3UfLyFb9)ie6A=aP>#aN4V2%!Y)QHnB@V#x^N0f^q?1g=*JnP z99Q(C554F?5~t9GPPF3$5@QRU7h{8iHwxR~r z*n}#CQGtypM;S`79wi829oC{4MOcGEo@2RvKR%K!iX literal 0 HcmV?d00001 diff --git a/tests/TestFiles/wavetables/surge.wav b/tests/TestFiles/wavetables/surge.wav new file mode 100644 index 0000000000000000000000000000000000000000..18ff6c51d95d2e74c2be5ee4be4e7b0e94a9ecf2 GIT binary patch literal 1080 zcmXAndsGty7{%|WfYK<$B8Yg1sEsV*Q9O#m8KNXwiXagY*g)bD1k>>t+kk=Z?ANKBKg zSO$Ph3f2OpQG`WUhOErp2}^JF|A#nU62Gw6#8|a>lSQ(n#;Vb}XZeKfyuHBD!NqN* zaP=CY%UT|8)WXXm^@joI!BeAA{Fg5+tZr0BXc4nHoZ5^Ej2BIi@greUvexD%@vV=hmRGmQcryxxoV>yP(KGO~4u7F`t{BHXbf(*9e8H-Bvod z2ps3_Cv1C`H(G0~k}Wq`tTHWdexYst&<)9K#C89x&QnV+84VXN==CF`qq_0oYeVLP zu6-fBlJ16cSI&BOl(bK_g`Mtd7M<#EO3(@#I_l*ox74^Fx2?85W?$uXB>HegrM|+i z{KP@$1KRzYH49~(>dI2tK52=nxK^braw?J*8VbY(bINLEv{I-PD9L1@iK~@!N^yaq zKw9Wjq${dbsfwi~vVE1Mo$7_M%^K}~=L07W`jzV|Dk`H7dmXW_vOZ>8?S6bqjr?Rs zy`UjM+utNQ)zut!da|vg-Miz;*@knH?vP&BKJ&qAL*v7vx{*=6{^A9rVaX-uDgUbx z*Cn%uZnVwc`obbCh8fIZfhAZ9OWXzltZ+N-fVJs3Hdu~3aTjcHH|$`Kd*Fb3;Rq)< z!v!ngij{D~Dy+sD+y^1taX;4L0Xzr~JOod8!5cn!7>~dge(*;C9>rq_#N&7ZL0HG@ zSj6l3Ne<=>9KsuU6NmCsyqUvz3vcD8`5E5E+c}(fa0Ey4PLASdIhvp27=E5(`2~*S zc;3Y?vX~P%k#}2%ER7S6ohW68Ibbt=h>r_q^^afSZAv#QN(h+)#s^};kqqnJ=-l5}kf@-Li zPEsA!)4TK@HBcjI>3wRV59mWWMITW!wNNXarjMzOKA|(zPM^|e)Ips%i!OYQbNB*Z zq8neK2fg?j-=Giu7{DOD#Sp&3_ZY?x_z^mc;3tgYXPk!~zhDd(FpdcrFo|Dr5tnco lS1^TX{D!Oe9oKLjGx!6u_!CClz#RU @@ -54,3 +55,34 @@ TEST_CASE("[Wavetables] Octave number lookup") REQUIRE(oct == Approx(ref).margin(0.03f)); } } + +TEST_CASE("[Wavetables] Wavetable sound files: Surge") +{ + sfz::FileMetadataReader reader; + sfz::WavetableInfo wt; + + REQUIRE(reader.open("tests/TestFiles/wavetables/surge.wav")); + REQUIRE(reader.extractWavetableInfo(wt)); + + REQUIRE(wt.tableSize == 256); +} + +TEST_CASE("[Wavetables] Wavetable sound files: Clm") +{ + sfz::FileMetadataReader reader; + sfz::WavetableInfo wt; + + REQUIRE(reader.open("tests/TestFiles/wavetables/clm.wav")); + REQUIRE(reader.extractWavetableInfo(wt)); + + REQUIRE(wt.tableSize == 256); +} + +TEST_CASE("[Wavetables] Non-wavetable sound files") +{ + sfz::FileMetadataReader reader; + sfz::WavetableInfo wt; + + REQUIRE(reader.open("tests/TestFiles/snare.wav")); + REQUIRE(!reader.extractWavetableInfo(wt)); +} From 9b115303310e185e795c8692b71b4b676bb23bcf Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 23:17:31 +0200 Subject: [PATCH 074/445] Honor the base gain and volume before modifiers --- src/sfizz/Voice.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 97fa044d..b4f9598a 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -360,12 +360,14 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept egEnvelope.getBlock(modulationSpan); // Amplitude envelope + applyGain1(baseGain, modulationSpan); if (float* mod = mm.getModulationByKey(amplitudeKey)) { for (size_t i = 0; i < numSamples; ++i) modulationSpan[i] *= normalizePercents(mod[i]); } // Volume envelope + applyGain1(db2mag(baseVolumedB), modulationSpan); if (float* mod = mm.getModulationByKey(volumeKey)) { for (size_t i = 0; i < numSamples; ++i) modulationSpan[i] *= db2mag(mod[i]); From a8c524b7e8856e54a01fde9eda7a12ce16a2dbd4 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 23:29:28 +0200 Subject: [PATCH 075/445] Slightly looser masking condition --- src/sfizz/Synth.cpp | 2 +- tests/PolyphonyT.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c1983016..c244620b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -930,7 +930,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc notePolyphonyCounter += 1; switch (region->selfMask) { case SfzSelfMask::mask: - if (voice->getTriggerValue() < velocity) { + if (voice->getTriggerValue() <= velocity) { if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) selfMaskCandidate = voice.get(); } diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index aeac0ebe..15173915 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -224,3 +224,21 @@ TEST_CASE("[Polyphony] Not self-masking") REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } + +TEST_CASE("[Polyphony] Self-masking with the exact same velocity") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + 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(true) == 3); // One of these is releasing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 63_norm); + REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); +} From 34d6ed02918620ad4437bfcd12f4e4b6d0c7c1cc Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 9 Aug 2020 23:45:19 +0200 Subject: [PATCH 076/445] Add the program to display file wavetable info --- tests/CMakeLists.txt | 3 +++ tests/FileWavetable.cpp | 52 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/FileWavetable.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 31e134c8..08623b6d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -93,6 +93,9 @@ target_link_libraries(sfizz_plot_wavetables PRIVATE sfizz::sfizz) add_executable(sfizz_file_instrument FileInstrument.cpp) target_link_libraries(sfizz_file_instrument PRIVATE sfizz::sfizz) +add_executable(sfizz_file_wavetable FileWavetable.cpp) +target_link_libraries(sfizz_file_wavetable PRIVATE sfizz::sfizz) + add_executable(sfizz_tuning Tuning.cpp) target_link_libraries(sfizz_tuning PRIVATE sfizz::sfizz) diff --git a/tests/FileWavetable.cpp b/tests/FileWavetable.cpp new file mode 100644 index 00000000..9e1fa597 --- /dev/null +++ b/tests/FileWavetable.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/FileMetadata.h" +#include "absl/strings/string_view.h" +#include + +static void printWavetable(const sfz::WavetableInfo& wt) +{ + printf("Table size: %u\n", wt.tableSize); + printf("Cross-table interpolation: %d\n", wt.crossTableInterpolation); + printf("One-shot: %d\n", wt.oneShot); +} + +static void usage(const char* argv0) +{ + fprintf( + stderr, + "Usage: %s \n", + argv0); +} + +int main(int argc, char *argv[]) +{ + fs::path path; + + if (argc == 2) + path = argv[1]; + else { + usage(argv[0]); + return 1; + } + + sfz::WavetableInfo wt {}; + + sfz::FileMetadataReader reader; + if (!reader.open(path)) { + fprintf(stderr, "Cannot open file\n"); + return 1; + } + if (!reader.extractWavetableInfo(wt)) { + fprintf(stderr, "Cannot get wavetable info\n"); + return 1; + } + + printWavetable(wt); + + return 0; +} From 71ae5943c97066e7f22a5d76c49929da48ef01bf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 10 Aug 2020 00:01:21 +0200 Subject: [PATCH 077/445] Extract the table size off U-he metadata --- src/sfizz/FileMetadata.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FileMetadata.cpp b/src/sfizz/FileMetadata.cpp index 87379796..a03069c8 100644 --- a/src/sfizz/FileMetadata.cpp +++ b/src/sfizz/FileMetadata.cpp @@ -377,9 +377,16 @@ bool FileMetadataReader::Impl::extractUheWavetable(WavetableInfo &wt) if (!uhwt) return false; - // u-he Hive: no idea what is inside this one, 2048 assumed + // zeros (chunk version?), 4 bytes LE + // number of tables, 4 bytes LE + // table size, 4 bytes LE + + uint8_t data[12]; + if (readRiffData(uhwt->index, data, sizeof(data)) != sizeof(data)) + return false; + + wt.tableSize = u32le(data + 8); - wt.tableSize = 2048; wt.crossTableInterpolation = 0; wt.oneShot = false; From a81149044a081c58fdd49d1939002fe38e9cb53f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 9 Aug 2020 23:17:31 +0200 Subject: [PATCH 078/445] Honor the base gain and volume before modifiers --- src/sfizz/Voice.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 97fa044d..b4f9598a 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -360,12 +360,14 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept egEnvelope.getBlock(modulationSpan); // Amplitude envelope + applyGain1(baseGain, modulationSpan); if (float* mod = mm.getModulationByKey(amplitudeKey)) { for (size_t i = 0; i < numSamples; ++i) modulationSpan[i] *= normalizePercents(mod[i]); } // Volume envelope + applyGain1(db2mag(baseVolumedB), modulationSpan); if (float* mod = mm.getModulationByKey(volumeKey)) { for (size_t i = 0; i < numSamples; ++i) modulationSpan[i] *= db2mag(mod[i]); From 6e392f95632e6ed1ca6c2a31478540023c1ead77 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 10 Aug 2020 00:25:04 +0200 Subject: [PATCH 079/445] Fix a minor mistake about opcode scope --- src/sfizz/Synth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 606d3497..fce8dd91 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -218,7 +218,7 @@ void sfz::Synth::clear() void sfz::Synth::handleMasterOpcodes(const std::vector& members) { for (auto& rawMember : members) { - const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal); + const Opcode member = rawMember.cleanUp(kOpcodeScopeMaster); switch (member.lettersOnlyHash) { case hash("polyphony"): From e08314f2c8b68d5a07324b60a3645d22961118b4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 10 Aug 2020 00:57:27 +0200 Subject: [PATCH 080/445] Fix pathnames such that tests can run from the project root --- tests/PolyphonyT.cpp | 22 +++++++++++----------- tests/SynthT.cpp | 38 +++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 15173915..0fa3aa01 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -17,7 +17,7 @@ constexpr int blockSize { 256 }; TEST_CASE("[Polyphony] Polyphony in hierarchy") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( key=61 sample=*sine polyphony=2 polyphony=2 key=62 sample=*sine @@ -44,7 +44,7 @@ TEST_CASE("[Polyphony] Polyphony in hierarchy") TEST_CASE("[Polyphony] Polyphony groups") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 key=62 sample=*sine group=1 polyphony=3 @@ -71,7 +71,7 @@ TEST_CASE("[Polyphony] Polyphony groups") TEST_CASE("[Polyphony] group polyphony limits") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( group=1 polyphony=2 sample=*sine key=65 )"); @@ -84,7 +84,7 @@ TEST_CASE("[Polyphony] group polyphony limits") TEST_CASE("[Polyphony] Hierarchy polyphony limits") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 )"); @@ -97,7 +97,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits") TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 )"); @@ -110,7 +110,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 sample=*sine key=65 @@ -124,7 +124,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*saw key=65 @@ -143,7 +143,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 sample=*sine key=65 @@ -159,7 +159,7 @@ TEST_CASE("[Polyphony] Polyphony in master") sfz::Synth synth; synth.setSamplesPerBlock(blockSize); sfz::AudioBuffer buffer { 2, blockSize }; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 group=2 sample=*sine key=65 @@ -192,7 +192,7 @@ TEST_CASE("[Polyphony] Polyphony in master") TEST_CASE("[Polyphony] Self-masking") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*sine key=64 note_polyphony=2 )"); synth.noteOn(0, 64, 63); @@ -210,7 +210,7 @@ TEST_CASE("[Polyphony] Self-masking") TEST_CASE("[Polyphony] Not self-masking") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*sine key=66 note_polyphony=2 note_selfmask=off )"); synth.noteOn(0, 66, 63); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index ef63db86..a6ec1aa2 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -540,7 +540,7 @@ TEST_CASE("[Synth] sample quality") TEST_CASE("[Synth] Sister voices") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sister_voices.sfz", R"( key=61 sample=*sine key=62 sample=*sine key=62 sample=*sine @@ -577,7 +577,7 @@ TEST_CASE("[Synth] Apply function on sisters") { sfz::Synth synth; sfz::AudioBuffer buffer { 2, 256 }; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sister_voices.sfz", R"( key=63 sample=*saw key=63 sample=*saw key=63 sample=*saw @@ -595,7 +595,7 @@ TEST_CASE("[Synth] Sisters and off-by") { sfz::Synth synth; sfz::AudioBuffer buffer { 2, 256 }; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sister_voices.sfz", R"( key=62 sample=*sine group=1 off_by=2 key=62 sample=*sine group=2 key=63 sample=*saw @@ -615,7 +615,7 @@ TEST_CASE("[Synth] Sisters and off-by") TEST_CASE("[Synth] Release key") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( key=62 sample=*sine trigger=release_key )"); synth.noteOn(0, 62, 85); @@ -627,7 +627,7 @@ TEST_CASE("[Synth] Release key") TEST_CASE("[Synth] Release") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( key=62 sample=*silence key=62 sample=*sine trigger=release )"); @@ -642,7 +642,7 @@ TEST_CASE("[Synth] Release") TEST_CASE("[Synth] Release (pedal was already down)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( key=62 sample=*silence key=62 sample=*sine trigger=release )"); @@ -659,7 +659,7 @@ TEST_CASE("[Synth] Release (pedal was already down)") TEST_CASE("[Synth] Release samples don't play unless there is another playing region that matches") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); @@ -675,7 +675,7 @@ TEST_CASE("[Synth] Release samples don't play unless there is another playing re TEST_CASE("[Synth] Release key (Different sustain CC)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( sustain_cc=54 key=62 sample=*sine trigger=release_key )"); @@ -688,7 +688,7 @@ TEST_CASE("[Synth] Release key (Different sustain CC)") TEST_CASE("[Synth] Release (Different sustain CC)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( sustain_cc=54 key=62 sample=*silence key=62 sample=*sine trigger=release @@ -704,7 +704,7 @@ TEST_CASE("[Synth] Release (Different sustain CC)") TEST_CASE("[Synth] Sustain threshold default") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); @@ -716,7 +716,7 @@ TEST_CASE("[Synth] Sustain threshold default") TEST_CASE("[Synth] Sustain threshold") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( sustain_lo=63 key=62 sample=*silence key=62 sample=*sine trigger=release @@ -762,7 +762,7 @@ const std::vector getActiveVoices(const sfz::Synth& synth) TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( lokey=62 hikey=64 sample=*sine trigger=release_key )"); synth.noteOn(0, 62, 85); @@ -786,7 +786,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices after)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( lokey=62 hikey=64 sample=*silence lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 @@ -816,7 +816,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared the delayed voices after)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( lokey=62 hikey=64 sample=*silence lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 @@ -846,7 +846,7 @@ TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( lokey=62 hikey=64 sample=*silence lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 @@ -876,8 +876,8 @@ TEST_CASE("[Synth] No release sample after the main sample stopped sounding by d synth.setSamplesPerBlock(4096); sfz::AudioBuffer buffer { 2, 4096 }; - synth.loadSfzString(fs::current_path(), R"( - lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + lokey=62 hikey=64 sample=closedhat.wav loop_mode=one_shot lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 )"); @@ -911,8 +911,8 @@ TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the a synth.setSamplesPerBlock(4096); sfz::AudioBuffer buffer { 2, 4096 }; - synth.loadSfzString(fs::current_path(), R"( - lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + lokey=62 hikey=64 sample=closedhat.wav loop_mode=one_shot lokey=62 hikey=64 sample=*sine trigger=release loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 )"); From e660b80042fcae376a7c46a4277bd0ff834cf9aa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 19 Jul 2020 19:36:27 +0200 Subject: [PATCH 081/445] Check all used CCs and output them in the MIDNAM file if they have no name --- src/sfizz/Synth.cpp | 73 ++++++++++++++++++++++++ src/sfizz/Synth.h | 16 ++++++ src/sfizz/modulations/ModMatrix.cpp | 24 ++++++++ src/sfizz/modulations/ModMatrix.h | 28 +++++++++ tests/SynthT.cpp | 88 +++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c746f273..d44d6d8f 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1195,14 +1195,26 @@ std::string sfz::Synth::exportMidnam(absl::string_view model) const } { + auto anonymousCCs = getUsedCCs(); + pugi::xml_node cns = device.append_child("ControlNameList"); cns.append_attribute("Name").set_value("Controls"); for (const auto& pair : ccLabels) { + anonymousCCs.set(pair.first, false); pugi::xml_node cn = cns.append_child("Control"); cn.append_attribute("Type").set_value("7bit"); cn.append_attribute("Number").set_value(std::to_string(pair.first).c_str()); cn.append_attribute("Name").set_value(pair.second.c_str()); } + + for (unsigned i = 0; i < anonymousCCs.size(); ++i) { + if (anonymousCCs[i]) { + pugi::xml_node cn = cns.append_child("Control"); + cn.append_attribute("Type").set_value("7bit"); + cn.append_attribute("Number").set_value(std::to_string(i).c_str()); + cn.append_attribute("Name").set_value(("Unnamed CC " + std::to_string(i)).c_str()); + } + } } { @@ -1581,3 +1593,64 @@ void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexce polyphonyGroups[groupIdx].setPolyphonyLimit(polyphony); } + +std::bitset sfz::Synth::getUsedCCs() const noexcept +{ + std::bitset used; + for (const RegionPtr& region : regions) + updateUsedCCsFromRegion(used, *region); + updateUsedCCsFromModulations(used, resources.modMatrix); + return used; +} + +void sfz::Synth::updateUsedCCsFromRegion(std::bitset& usedCCs, const Region& region) +{ + updateUsedCCsFromCCMap(usedCCs, region.offsetCC); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccHold); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccStart); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccSustain); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccHold); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccStart); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccSustain); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccHold); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccStart); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccSustain); + updateUsedCCsFromCCMap(usedCCs, region.ccConditions); + updateUsedCCsFromCCMap(usedCCs, region.ccTriggers); + updateUsedCCsFromCCMap(usedCCs, region.crossfadeCCInRange); + updateUsedCCsFromCCMap(usedCCs, region.crossfadeCCOutRange); +} + +void sfz::Synth::updateUsedCCsFromModulations(std::bitset& usedCCs, const ModMatrix& mm) +{ + class CCSourceCollector : public ModMatrix::KeyVisitor { + public: + explicit CCSourceCollector(std::bitset& used) + : used_(used) + { + } + + bool visit(const ModKey& key) override + { + if (key.id() == ModId::Controller) + used_.set(key.parameters().cc); + return true; + } + std::bitset& used_; + }; + + CCSourceCollector vtor(usedCCs); + mm.visitSources(vtor); +} diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 8133b676..3b7ce3b8 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -575,6 +575,13 @@ public: */ const std::vector& getCCLabels() const noexcept { return ccLabels; } + /** + * @brief Get the used CCs + * + * @return const std::bitset& + */ + std::bitset getUsedCCs() const noexcept; + protected: /** * @brief The voice callback which is called during a change of state. @@ -697,6 +704,15 @@ private: void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; + template + static void updateUsedCCsFromCCMap(std::bitset& usedCCs, const CCMap map) + { + for (auto& mod : map) + usedCCs[mod.cc] = true; + } + static void updateUsedCCsFromRegion(std::bitset& usedCCs, const Region& region); + static void updateUsedCCsFromModulations(std::bitset& usedCCs, const ModMatrix& mm); + // 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; diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 41b1c844..694a9cc0 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -428,4 +428,28 @@ std::string ModMatrix::toDotGraph() const return dot; } +bool ModMatrix::visitSources(KeyVisitor& vtor) const +{ + const Impl& impl = *impl_; + + for (const Impl::Source& item : impl.sources_) { + if (!vtor.visit(item.key)) + return false; + } + + return true; +} + +bool ModMatrix::visitTargets(KeyVisitor& vtor) const +{ + const Impl& impl = *impl_; + + for (const Impl::Target& item : impl.targets_) { + if (!vtor.visit(item.key)) + return false; + } + + return true; +} + } // namespace sfz diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index fa1f2633..0a451bec 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -173,6 +173,34 @@ public: */ std::string toDotGraph() const; + class KeyVisitor { + public: + virtual ~KeyVisitor() {} + /** + * @brief Visit a key of the modulation matrix. + * + * @param key + * @return true to continue visiting, false to stop + */ + virtual bool visit(const ModKey& key) = 0; + }; + + /** + * @brief Visit the keys of all the sources in the matrix. + * + * @param vtor a visitor object + * @return last return code from the visitor + */ + bool visitSources(KeyVisitor& vtor) const; + + /** + * @brief Visit the keys of all the sources in the matrix. + * + * @param vtor a visitor object + * @return last return code from the visitor + */ + bool visitTargets(KeyVisitor& vtor) const; + private: struct Impl; std::unique_ptr impl_; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index a6ec1aa2..2d39c754 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -981,3 +981,91 @@ TEST_CASE("[Synth] sw_default works at a group level") synth.noteOn(0, 62, 85); REQUIRE( synth.getNumActiveVoices(true) == 1 ); } + +TEST_CASE("[Synth] Used CCs") +{ + sfz::Synth synth; + REQUIRE( !synth.getUsedCCs().any() ); + synth.loadSfzString(fs::current_path(), R"( + amplitude_cc1=100 + volume_oncc2=5 + locc4=64 hicc67=32 pan_cc5=200 sample=*sine + width_cc98=200 sample=*sine + position_cc42=200 pitch_oncc56=200 sample=*sine + start_locc44=200 hikey=-1 sample=*sine + )"); + auto usedCCs = synth.getUsedCCs(); + REQUIRE( usedCCs[1] ); + REQUIRE( usedCCs[2] ); + REQUIRE( !usedCCs[3] ); + REQUIRE( usedCCs[4] ); + REQUIRE( usedCCs[5] ); + REQUIRE( !usedCCs[6] ); + REQUIRE( usedCCs[42] ); + REQUIRE( usedCCs[44] ); + REQUIRE( usedCCs[56] ); + REQUIRE( usedCCs[67] ); + REQUIRE( usedCCs[98] ); + REQUIRE( !usedCCs[127] ); +} + +TEST_CASE("[Synth] Used CCs EGs") +{ + sfz::Synth synth; + REQUIRE( !synth.getUsedCCs().any() ); + synth.loadSfzString(fs::current_path(), R"( + + ampeg_attack_oncc1=1 + ampeg_sustain_oncc2=2 + ampeg_start_oncc3=3 + ampeg_hold_oncc4=4 + ampeg_decay_oncc5=5 + ampeg_delay_oncc6=6 + ampeg_release_oncc7=7 + sample=*sine + + pitcheg_attack_oncc11=11 + pitcheg_sustain_oncc12=12 + pitcheg_start_oncc13=13 + pitcheg_hold_oncc14=14 + pitcheg_decay_oncc15=15 + pitcheg_delay_oncc16=16 + pitcheg_release_oncc17=17 + sample=*sine + + fileg_attack_oncc21=21 + fileg_sustain_oncc22=22 + fileg_start_oncc23=23 + fileg_hold_oncc24=24 + fileg_decay_oncc25=25 + fileg_delay_oncc26=26 + fileg_release_oncc27=27 + sample=*sine + )"); + auto usedCCs = synth.getUsedCCs(); + REQUIRE( usedCCs[1] ); + REQUIRE( usedCCs[2] ); + REQUIRE( usedCCs[3] ); + REQUIRE( usedCCs[4] ); + REQUIRE( usedCCs[5] ); + REQUIRE( usedCCs[6] ); + REQUIRE( usedCCs[7] ); + // FIXME: enable when supported + // REQUIRE( !usedCCs[8] ); + // REQUIRE( usedCCs[11] ); + // REQUIRE( usedCCs[12] ); + // REQUIRE( usedCCs[13] ); + // REQUIRE( usedCCs[14] ); + // REQUIRE( usedCCs[15] ); + // REQUIRE( usedCCs[16] ); + // REQUIRE( usedCCs[17] ); + // REQUIRE( !usedCCs[18] ); + // REQUIRE( usedCCs[21] ); + // REQUIRE( usedCCs[22] ); + // REQUIRE( usedCCs[23] ); + // REQUIRE( usedCCs[24] ); + // REQUIRE( usedCCs[25] ); + // REQUIRE( usedCCs[26] ); + // REQUIRE( usedCCs[27] ); + // REQUIRE( !usedCCs[28] ); +} From 7a1a2c9381c84718464fb5337363121ef5c3085f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 15:22:35 +0200 Subject: [PATCH 082/445] Finer tests and behavior for note_polyphony- note_polyphony is shared amoung polyphony groups- Lower velocity samples do not mute higher velocity samples, but they still play! --- src/sfizz/Synth.cpp | 11 ++--- tests/PolyphonyT.cpp | 100 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c746f273..a184803b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -925,6 +925,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc if (region->notePolyphony) { if (!voice->releasedOrFree() + && voice->getRegion()->group == region->group && voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { notePolyphonyCounter += 1; @@ -948,11 +949,11 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } // Polyphony reached on note_polyphony - if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { - if (selfMaskCandidate != nullptr) - selfMaskCandidate->release(delay); - else // We're the lowest velocity guy here - continue; + // If there's a self-masking candidate, release it + if (region->notePolyphony + && notePolyphonyCounter >= *region->notePolyphony + && selfMaskCandidate != nullptr) { + selfMaskCandidate->release(delay); } auto parent = region->parent; diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 0fa3aa01..f1e261d1 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -218,7 +218,7 @@ TEST_CASE("[Polyphony] Not self-masking") synth.noteOn(0, 66, 64); REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); @@ -242,3 +242,101 @@ TEST_CASE("[Polyphony] Self-masking with the exact same velocity") REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } + +TEST_CASE("[Polyphony] Self-masking only works from low to high") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sample=*sine key=64 note_polyphony=1 + )"); + synth.noteOn(0, 64, 63); + synth.noteOn(0, 64, 62); + REQUIRE(synth.getNumActiveVoices(true) == 2); // Both notes are playing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sample=*saw key=64 note_polyphony=1 + sample=*sine key=64 note_polyphony=1 + )"); + synth.noteOn(0, 64, 62); + synth.noteOn(0, 64, 63); + REQUIRE(synth.getNumActiveVoices(true) == 4); // Both notes are playing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE(synth.getVoiceView(2)->releasedOrFree()); // got killed + REQUIRE(synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default, with keyswitches)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sw_lokey=36 sw_hikey=37 sw_default=36 + sw_last=36 key=48 note_polyphony=1 sample=*saw + sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri + )"); + synth.noteOn(0, 48, 63); + REQUIRE(synth.getNumActiveVoices(true) == 1); + synth.cc(0, 64, 127); + synth.noteOn(0, 37, 127); + synth.noteOff(0, 37, 0); + synth.noteOn(0, 48, 64); + REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + + +TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + group=1 sample=*saw key=64 note_polyphony=1 + group=2 sample=*sine key=64 note_polyphony=1 + )"); + synth.noteOn(0, 64, 62); + synth.noteOn(0, 64, 63); + REQUIRE(synth.getNumActiveVoices(true) == 4); // Both notes are playing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); + REQUIRE(synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (with keyswitches)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sw_lokey=36 sw_hikey=37 sw_default=36 + group=1 sw_last=36 key=48 note_polyphony=1 sample=*saw + group=2 sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri + )"); + synth.noteOn(0, 48, 63); + REQUIRE(synth.getNumActiveVoices(true) == 1); + synth.cc(0, 64, 127); + synth.noteOn(0, 37, 127); + synth.noteOff(0, 37, 0); + synth.noteOn(0, 48, 64); + REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} From d3cc4281d97fa74748d6620f285b1cf89da434bc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 16:14:03 +0200 Subject: [PATCH 083/445] Parse off time --- src/sfizz/Defaults.h | 3 ++- src/sfizz/Region.cpp | 6 ++++++ src/sfizz/Region.h | 1 + tests/RegionT.cpp | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index cc0ac0b3..18ec3df8 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -31,7 +31,7 @@ enum class SfzTrigger { attack, release, release_key, first, legato }; enum class SfzLoopMode { no_loop, one_shot, loop_continuous, loop_sustain }; -enum class SfzOffMode { fast, normal }; +enum class SfzOffMode { fast, normal, time }; enum class SfzVelocityOverride { current, previous }; enum class SfzCrossfadeCurve { gain, power }; enum class SfzSelfMask { mask, dontMask }; @@ -75,6 +75,7 @@ namespace Default constexpr uint32_t group { 0 }; constexpr Range groupRange { 0, std::numeric_limits::max() }; constexpr SfzOffMode offMode { SfzOffMode::fast }; + constexpr float offTime { 6e-3f }; constexpr Range polyphonyRange { 0, config::maxVoices }; constexpr SfzSelfMask selfMask { SfzSelfMask::mask }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 0c1a228f..8452311b 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -161,10 +161,16 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("normal"): offMode = SfzOffMode::normal; break; + case hash("time"): + offMode = SfzOffMode::time; + break; default: DBG("Unkown off mode:" << opcode.value); } break; + case hash("off_time"): + setValueFromOpcode(opcode, offTime, Default::egTimeRange); + break; case hash("polyphony"): if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) polyphony = *value; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 76113910..72c29ad9 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -286,6 +286,7 @@ struct Region { uint32_t group { Default::group }; // group absl::optional offBy {}; // off_by SfzOffMode offMode { Default::offMode }; // off_mode + float offTime { Default::offTime }; // off_mode absl::optional notePolyphony {}; // note_polyphony unsigned polyphony { config::maxVoices }; // polyphony SfzSelfMask selfMask { Default::selfMask }; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 01469495..c6317a0f 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -195,6 +195,20 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.offMode == SfzOffMode::fast); region.parseOpcode({ "off_mode", "normal" }); REQUIRE(region.offMode == SfzOffMode::normal); + region.parseOpcode({ "off_mode", "time" }); + REQUIRE(region.offMode == SfzOffMode::time); + } + + SECTION("off_time") + { + REQUIRE(region.offTime == 0.006f); + region.parseOpcode({ "off_time", "0.1" }); + REQUIRE(region.offTime == 0.1f); + region.parseOpcode({ "off_time", "0" }); + REQUIRE(region.offTime == 0.0f); + region.parseOpcode({ "off_time", "0.1" }); + region.parseOpcode({ "off_time", "-1" }); + REQUIRE(region.offTime == 0.0f); } SECTION("lokey, hikey, and key") From 5ef6141c4b07bd8a2147aaabbb8202dd23fbc70c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 16:14:55 +0200 Subject: [PATCH 084/445] Add a way to change the release rate after the envelope started and separate this from the `release()` method in the Voice --- src/sfizz/ADSREnvelope.cpp | 43 ++++++++++++++++++++++++-------------- src/sfizz/ADSREnvelope.h | 16 ++++++++++---- src/sfizz/Voice.cpp | 20 ++++++++++++++---- src/sfizz/Voice.h | 10 ++++++++- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index bb86115b..bfd7f3f8 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -11,22 +11,30 @@ namespace sfz { +template +Type ADSREnvelope::secondsToSamples (Type timeInSeconds) const noexcept +{ + return static_cast(timeInSeconds * sampleRate); +}; + +template +Type ADSREnvelope::secondsToLinRate (Type timeInSeconds) const noexcept +{ + timeInSeconds = std::max(timeInSeconds, config::virtuallyZero); + return 1 / (sampleRate * timeInSeconds); +}; + +template +Type ADSREnvelope::secondsToExpRate (Type timeInSeconds) const noexcept +{ + timeInSeconds = std::max(25e-3, timeInSeconds); + return std::exp(-8.0 / (timeInSeconds * sampleRate)); +}; + template void ADSREnvelope::reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept { - auto secondsToSamples = [sampleRate](Type timeInSeconds) { - return static_cast(timeInSeconds * sampleRate); - }; - - auto secondsToLinRate = [sampleRate](Type timeInSeconds) { - timeInSeconds = std::max(timeInSeconds, config::virtuallyZero); - return 1 / (sampleRate * timeInSeconds); - }; - - auto secondsToExpRate = [sampleRate](Type timeInSeconds) { - timeInSeconds = std::max(25e-3, timeInSeconds); - return std::exp(-8.0 / (timeInSeconds * sampleRate)); - }; + this->sampleRate = sampleRate; this->delay = delay + secondsToSamples(desc.getDelay(state, velocity)); this->attackStep = secondsToLinRate(desc.getAttack(state, velocity)); @@ -208,13 +216,16 @@ int ADSREnvelope::getRemainingDelay() const noexcept } template -void ADSREnvelope::startRelease(int releaseDelay, bool fastRelease) noexcept +void ADSREnvelope::startRelease(int releaseDelay) noexcept { shouldRelease = true; this->releaseDelay = releaseDelay; +} - if (fastRelease) - this->releaseRate = 0; +template +void ADSREnvelope::setReleaseTime(Type timeInSeconds) noexcept +{ + releaseRate = secondsToExpRate(timeInSeconds); } } diff --git a/src/sfizz/ADSREnvelope.h b/src/sfizz/ADSREnvelope.h index 608567c6..892388f7 100644 --- a/src/sfizz/ADSREnvelope.h +++ b/src/sfizz/ADSREnvelope.h @@ -44,15 +44,18 @@ public: * @param output */ void getBlock(absl::Span output) noexcept; + /** + * @brief Set the release time for the envelope + * + * @param timeInSeconds + */ + void setReleaseTime(Type timeInSeconds) noexcept; /** * @brief Start the envelope release after a delay. * * @param releaseDelay the delay before releasing in samples - * @param fastRelease whether the release should be fast (i.e. 0 or so) or - * follow the release duration that was set when - * initializing the envelope */ - void startRelease(int releaseDelay, bool fastRelease = false) noexcept; + void startRelease(int releaseDelay) noexcept; /** * @brief Is the envelope smoothing? * @@ -75,6 +78,11 @@ public: int getRemainingDelay() const noexcept; private: + float sampleRate { config::defaultSampleRate }; + Type secondsToSamples (Type timeInSeconds) const noexcept; + Type secondsToLinRate (Type timeInSeconds) const noexcept; + Type secondsToExpRate (Type timeInSeconds) const noexcept; + enum class State { Delay, Attack, diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index b4f9598a..d9ade7f8 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -155,7 +155,7 @@ bool sfz::Voice::isFree() const noexcept return (state == State::idle); } -void sfz::Voice::release(int delay, bool fastRelease) noexcept +void sfz::Voice::release(int delay) noexcept { if (state != State::playing) return; @@ -163,10 +163,21 @@ void sfz::Voice::release(int delay, bool fastRelease) noexcept if (egEnvelope.getRemainingDelay() > delay) { switchState(State::cleanMeUp); } else { - egEnvelope.startRelease(delay, fastRelease); + egEnvelope.startRelease(delay); } } +void sfz::Voice::off(int delay) noexcept +{ + if (region->offMode == SfzOffMode::fast) { + egEnvelope.setReleaseTime( Default::offTime ); + } else if (region->offMode == SfzOffMode::time) { + egEnvelope.setReleaseTime(region->offTime); + } + + release(delay); +} + void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept { ASSERT(velocity >= 0.0 && velocity <= 1.0); @@ -561,7 +572,8 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept << " for sample " << region->sampleId); } #endif - egEnvelope.startRelease(i, true); + egEnvelope.setReleaseTime(0.0f); + egEnvelope.startRelease(i); fill(indices->subspan(i), sampleEnd); fill(coeffs->subspan(i), 1.0f); break; @@ -695,7 +707,7 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept return false; if (triggerType == TriggerType::NoteOn && region->offBy == group) { - release(delay, region->offMode == SfzOffMode::fast); + off(delay); return true; } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 52a7f0c8..ce473c7b 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -288,7 +288,15 @@ public: * @param delay * @param fastRelease whether to do a normal release or cut the voice abruptly */ - void release(int delay, bool fastRelease = false) noexcept; + void release(int delay) noexcept; + + /** + * @brief Off the voice (steal). This will respect the off mode of the region + * and set the envelopes if necessary. + * + * @param delay + */ + void off(int delay) noexcept; /** * @brief gets the age of the Voice From 2cdf4f165fc37192244cd7c741d00310807267f6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:09:58 +0200 Subject: [PATCH 085/445] Add helpers to count playing voices in polyphony groups --- src/sfizz/PolyphonyGroup.cpp | 7 +++++++ src/sfizz/PolyphonyGroup.h | 4 ++++ src/sfizz/RegionSet.cpp | 7 +++++++ src/sfizz/RegionSet.h | 4 ++++ 4 files changed, 22 insertions(+) diff --git a/src/sfizz/PolyphonyGroup.cpp b/src/sfizz/PolyphonyGroup.cpp index 7ac1e3d6..d5b1615c 100644 --- a/src/sfizz/PolyphonyGroup.cpp +++ b/src/sfizz/PolyphonyGroup.cpp @@ -16,3 +16,10 @@ void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept { swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); } + +unsigned sfz::PolyphonyGroup::numPlayingVoices() const noexcept +{ + return absl::c_count_if(voices, [](const Voice* v) { + return !v->releasedOrFree(); + }); +} diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index d3e1413b..7a4281af 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -40,6 +40,10 @@ public: * @return unsigned */ unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; } + /** + * @brief Returns the number of playing (unreleased) voices + */ + unsigned numPlayingVoices() const noexcept; /** * @brief Get the active voices * diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp index d6f8a585..1387438a 100644 --- a/src/sfizz/RegionSet.cpp +++ b/src/sfizz/RegionSet.cpp @@ -46,3 +46,10 @@ void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice* parent = parent->getParent(); } } + +unsigned sfz::RegionSet::numPlayingVoices() const noexcept +{ + return absl::c_count_if(voices, [](const Voice* v) { + return !v->releasedOrFree(); + }); +} diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 24120173..bbfadce0 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -79,6 +79,10 @@ public: * @param parent */ void setParent(RegionSet* parent) noexcept { this->parent = parent; } + /** + * @brief Returns the number of playing (unreleased) voices + */ + unsigned numPlayingVoices() const noexcept; /** * @brief Get the active voices * From f318c528a26844a7f07c4244d18b3836e68abb4c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:10:19 +0200 Subject: [PATCH 086/445] Guard the voice stealing iif voices are empty --- src/sfizz/VoiceStealing.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 9cb88bb8..fbac880c 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -7,6 +7,9 @@ sfz::VoiceStealing::VoiceStealing() sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept { + if (voices.empty()) + return {}; + // Start of the voice stealing algorithm absl::c_stable_sort(voices, voiceOrdering); From 4529d371f026fe59329b146c6c9d687e287aaf7c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:10:40 +0200 Subject: [PATCH 087/445] Add a helper function to off all sister voices at once --- src/sfizz/SisterVoiceRing.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 78935335..93dbc515 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -56,6 +56,22 @@ struct SisterVoiceRing { return count; } + /** + * @brief Off all sisters in a ring + * + * @param voice + * @param delay + */ + template>::value, int> = 0> + static void offAllSisters(T* voice, int delay) { + if (voice != nullptr) { + SisterVoiceRing::applyToRing(voice, [&] (Voice* v) { + v->off(delay); + }); + } + } + /** * @brief Check if a sister voice ring is well formed * From 3fdf0ef7cd8b5427cdad7c759463b6de614e790d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:21:15 +0200 Subject: [PATCH 088/445] Remove the age guard --- src/sfizz/VoiceStealing.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index fbac880c..49e1b2d0 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -52,9 +52,5 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept while (idx < voices.size() && sisterVoices(ref, voices[idx])); } - // Guard for future changes: voices with age 0 just started; don't kill those. - if (returnedVoice->getAge() == 0) - return {}; - return returnedVoice; } From 812a86719474e3c95675f5eb6c328629512c921a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:22:05 +0200 Subject: [PATCH 089/445] Off voices rather than killing them --- src/sfizz/Synth.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index a184803b..7e257ded 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -919,7 +919,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc continue; } - if (voice->getRegion() == region) { + if (voice->getRegion() == region && !voice->releasedOrFree()) { regionPolyphonyArray.push_back(voice.get()); } @@ -953,31 +953,29 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate != nullptr) { - selfMaskCandidate->release(delay); + SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); } auto parent = region->parent; // Polyphony reached on region if (regionPolyphonyArray.size() >= region->polyphony) { - selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray)); - goto render; + const auto activeVoices = absl::MakeSpan(regionPolyphonyArray); + SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); } // Polyphony reached on polyphony group - if (polyphonyGroups[region->group].getActiveVoices().size() + if (polyphonyGroups[region->group].numPlayingVoices() == polyphonyGroups[region->group].getPolyphonyLimit()) { const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); - selectedVoice = stealer.steal(activeVoices); - goto render; + SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); } // Polyphony reached some parent group/master/etc while (parent != nullptr) { - if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { + if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) { const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); - selectedVoice = stealer.steal(activeVoices); - goto render; + SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); } parent = parent->getParent(); } @@ -987,7 +985,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); } - render: // For some reason we did not find a voice to use. // This is a degraded case but we'll just drop the note on. if (selectedVoice == nullptr) From 44dbbcaf54e0ad961558696462e4b3a85adb504a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:22:49 +0200 Subject: [PATCH 090/445] Updated tests --- tests/CMakeLists.txt | 4 +- tests/FilesT.cpp | 9 +- tests/PolyphonyT.cpp | 184 +++++++++++------- tests/RegionT.cpp | 2 +- tests/SynthT.cpp | 28 +-- tests/{RegionTHelpers.cpp => TestHelpers.cpp} | 20 +- tests/{RegionTHelpers.h => TestHelpers.h} | 30 +++ 7 files changed, 176 insertions(+), 101 deletions(-) rename tests/{RegionTHelpers.cpp => TestHelpers.cpp} (68%) rename tests/{RegionTHelpers.h => TestHelpers.h} (52%) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4018edee..dba296bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,8 +5,8 @@ project(sfizz) set(SFIZZ_TEST_SOURCES RegionT.cpp - RegionTHelpers.h - RegionTHelpers.cpp + TestHelpers.h + TestHelpers.cpp ParsingT.cpp HelpersT.cpp HelpersT.cpp diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index b47181fe..98b16dce 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -4,7 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "RegionTHelpers.h" +#include "TestHelpers.h" #include "sfizz/Synth.h" #include "sfizz/SfzHelpers.h" #include "sfizz/modulations/ModId.h" @@ -517,7 +517,8 @@ TEST_CASE("[Files] Off by with the same notes at the same time") synth.renderBlock(buffer); synth.noteOn(0, 65, 63); synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices(true) == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + REQUIRE( numPlayingVoices(synth) == 2 ); } TEST_CASE("[Files] Off modes") @@ -538,8 +539,10 @@ TEST_CASE("[Files] Off modes") synth.getVoiceView(0) ; synth.noteOn(100, 63, 63); REQUIRE( synth.getNumActiveVoices(true) == 3 ); + REQUIRE( numPlayingVoices(synth) == 1 ); AudioBuffer buffer { 2, 256 }; - synth.renderBlock(buffer); + for (unsigned i = 0; i < 10; ++i) // Not enough for the "normal" voice to die + synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices(true) == 2 ); REQUIRE( fastVoice->isFree() ); REQUIRE( !normalVoice->isFree() ); diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index f1e261d1..b45b2e4c 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -6,6 +6,8 @@ #include "sfizz/Synth.h" #include "sfizz/SfzHelpers.h" +#include "TestHelpers.h" +#include #include "catch2/catch.hpp" using namespace Catch::literals; @@ -13,7 +15,6 @@ using namespace sfz::literals; constexpr int blockSize { 256 }; - TEST_CASE("[Polyphony] Polyphony in hierarchy") { sfz::Synth synth; @@ -71,6 +72,7 @@ TEST_CASE("[Polyphony] Polyphony groups") TEST_CASE("[Polyphony] group polyphony limits") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( group=1 polyphony=2 sample=*sine key=65 @@ -78,12 +80,15 @@ TEST_CASE("[Polyphony] group polyphony limits") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 @@ -91,12 +96,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 @@ -104,12 +112,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 @@ -118,12 +129,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*saw key=65 @@ -137,12 +151,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") synth.noteOn(0, 66, 64); synth.noteOn(0, 66, 64); synth.noteOn(0, 66, 64); - REQUIRE(synth.getNumActiveVoices(true) == 5); + REQUIRE( synth.getNumActiveVoices(true) == 6); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 5); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 @@ -151,14 +168,16 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Polyphony in master") { sfz::Synth synth; - synth.setSamplesPerBlock(blockSize); sfz::AudioBuffer buffer { 2, blockSize }; + synth.setSamplesPerBlock(blockSize); synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 group=2 @@ -171,75 +190,90 @@ TEST_CASE("[Polyphony] Polyphony in master") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing synth.allSoundOff(); synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices(true) == 0); + REQUIRE( synth.getNumActiveVoices(true) == 0); synth.noteOn(0, 63, 64); synth.noteOn(0, 63, 64); synth.noteOn(0, 63, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing synth.allSoundOff(); synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices(true) == 0); + REQUIRE( synth.getNumActiveVoices(true) == 0); synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64); - REQUIRE(synth.getNumActiveVoices(true) == 3); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 3 ); } TEST_CASE("[Polyphony] Self-masking") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*sine key=64 note_polyphony=2 )"); - synth.noteOn(0, 64, 63); - synth.noteOn(0, 64, 62); + synth.noteOn(0, 64, 63 ); + synth.noteOn(0, 64, 62 ); synth.noteOn(0, 64, 64); - REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); - REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } TEST_CASE("[Polyphony] Not self-masking") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*sine key=66 note_polyphony=2 note_selfmask=off )"); - synth.noteOn(0, 66, 63); - synth.noteOn(0, 66, 62); + synth.noteOn(0, 66, 63 ); + synth.noteOn(0, 66, 62 ); synth.noteOn(0, 66, 64); - REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } TEST_CASE("[Polyphony] Self-masking with the exact same velocity") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path(), R"( 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(true) == 3); // One of these is releasing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 64_norm); + synth.noteOn(0, 64, 63 ); + synth.noteOn(0, 64, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } @@ -249,53 +283,59 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high") synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*sine key=64 note_polyphony=1 )"); - synth.noteOn(0, 64, 63); - synth.noteOn(0, 64, 62); - REQUIRE(synth.getNumActiveVoices(true) == 2); // Both notes are playing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + synth.noteOn(0, 64, 63 ); + synth.noteOn(0, 64, 62 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); // Both notes are playing + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*saw key=64 note_polyphony=1 sample=*sine key=64 note_polyphony=1 )"); - synth.noteOn(0, 64, 62); - synth.noteOn(0, 64, 63); - REQUIRE(synth.getNumActiveVoices(true) == 4); // Both notes are playing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 62_norm); - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // got killed - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); - REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // got killed - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(2)->releasedOrFree()); // got killed - REQUIRE(synth.getVoiceView(3)->getTriggerValue() == 63_norm); + synth.noteOn(0, 64, 62 ); + synth.noteOn(0, 64, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 4); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); } TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default, with keyswitches)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sw_lokey=36 sw_hikey=37 sw_default=36 sw_last=36 key=48 note_polyphony=1 sample=*saw sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri )"); - synth.noteOn(0, 48, 63); - REQUIRE(synth.getNumActiveVoices(true) == 1); + synth.noteOn(0, 48, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); synth.cc(0, 64, 127); synth.noteOn(0, 37, 127); synth.noteOff(0, 37, 0); synth.noteOn(0, 48, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } @@ -303,40 +343,46 @@ 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 buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( group=1 sample=*saw key=64 note_polyphony=1 group=2 sample=*sine key=64 note_polyphony=1 )"); - synth.noteOn(0, 64, 62); - synth.noteOn(0, 64, 63); - REQUIRE(synth.getNumActiveVoices(true) == 4); // Both notes are playing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 62_norm); - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // got killed - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); - REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // got killed - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); + synth.noteOn(0, 64, 62 ); + synth.noteOn(0, 64, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 4); // Both notes are playing + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); - REQUIRE(synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); } TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (with keyswitches)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sw_lokey=36 sw_hikey=37 sw_default=36 group=1 sw_last=36 key=48 note_polyphony=1 sample=*saw group=2 sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri )"); - synth.noteOn(0, 48, 63); - REQUIRE(synth.getNumActiveVoices(true) == 1); + synth.noteOn(0, 48, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); synth.cc(0, 64, 127); synth.noteOn(0, 37, 127); synth.noteOff(0, 37, 0); synth.noteOn(0, 48, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index c6317a0f..189fae32 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -4,7 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "RegionTHelpers.h" +#include "TestHelpers.h" #include "sfizz/MidiState.h" #include "sfizz/Region.h" #include "sfizz/SfzHelpers.h" diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index a6ec1aa2..bd949dd3 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -8,6 +8,7 @@ #include "sfizz/SisterVoiceRing.h" #include "sfizz/SfzHelpers.h" #include "sfizz/NumericId.h" +#include "TestHelpers.h" #include #include "catch2/catch.hpp" using namespace Catch::literals; @@ -607,7 +608,8 @@ TEST_CASE("[Synth] Sisters and off-by") REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.noteOn(0, 63, 85); REQUIRE( synth.getNumActiveVoices(true) == 3 ); - synth.renderBlock(buffer); + for (unsigned i = 0; i < 100; ++i) + synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices(true) == 2 ); REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 ); } @@ -735,30 +737,6 @@ TEST_CASE("[Synth] Sustain threshold") REQUIRE( synth.getNumActiveVoices(true) == 5 ); } -template -void sortAll(C& container) -{ - std::sort(container.begin(), container.end()); -} - -template -void sortAll(C& container, Args&... others) -{ - std::sort(container.begin(), container.end()); - sortAll(others...); -} - -const std::vector getActiveVoices(const sfz::Synth& synth) -{ - std::vector activeVoices; - for (int i = 0; i < synth.getNumVoices(); ++i) { - const auto* voice = synth.getVoiceView(i); - if (!voice->isFree()) - activeVoices.push_back(voice); - } - return activeVoices; -} - TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") { sfz::Synth synth; diff --git a/tests/RegionTHelpers.cpp b/tests/TestHelpers.cpp similarity index 68% rename from tests/RegionTHelpers.cpp rename to tests/TestHelpers.cpp index a47b56ed..ca32de0b 100644 --- a/tests/RegionTHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -4,7 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "RegionTHelpers.h" +#include "TestHelpers.h" #include "sfizz/modulations/ModId.h" size_t RegionCCView::size() const @@ -39,3 +39,21 @@ bool RegionCCView::match(const sfz::Region::Connection& conn) const { return conn.source.id() == sfz::ModId::Controller && conn.target == target_; } + +const std::vector getActiveVoices(const sfz::Synth& synth) +{ + std::vector activeVoices; + for (int i = 0; i < synth.getNumVoices(); ++i) { + const auto* voice = synth.getVoiceView(i); + if (!voice->isFree()) + activeVoices.push_back(voice); + } + return activeVoices; +} + +unsigned numPlayingVoices(const sfz::Synth& synth) +{ + return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) { + return !v->releasedOrFree(); + }); +} diff --git a/tests/RegionTHelpers.h b/tests/TestHelpers.h similarity index 52% rename from tests/RegionTHelpers.h rename to tests/TestHelpers.h index e9fcf897..ac8be3dc 100644 --- a/tests/RegionTHelpers.h +++ b/tests/TestHelpers.h @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include "sfizz/Synth.h" #include "sfizz/Region.h" #include "sfizz/modulations/ModKey.h" @@ -26,3 +27,32 @@ private: const sfz::Region& region_; sfz::ModKey target_; }; + +template +void sortAll(C& container) +{ + std::sort(container.begin(), container.end()); +} + +template +void sortAll(C& container, Args&... others) +{ + std::sort(container.begin(), container.end()); + sortAll(others...); +} + +/** + * @brief Get active voices from the synth + * + * @param synth + * @return const std::vector + */ +const std::vector getActiveVoices(const sfz::Synth& synth); + +/** + * @brief Count the number of playing (unreleased) voices from the synth + * + * @param synth + * @return unsigned + */ +unsigned numPlayingVoices(const sfz::Synth& synth); From 2e0278c7cf20e8b1fff822198726c6b056047c92 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:33:21 +0200 Subject: [PATCH 091/445] Passing off_time automatically sets the off_mode to time --- src/sfizz/Region.cpp | 1 + tests/RegionT.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 8452311b..c8aee196 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -169,6 +169,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) } break; case hash("off_time"): + offMode = SfzOffMode::time; setValueFromOpcode(opcode, offTime, Default::egTimeRange); break; case hash("polyphony"): diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 189fae32..481e8628 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -202,8 +202,10 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("off_time") { REQUIRE(region.offTime == 0.006f); + REQUIRE(region.offMode == SfzOffMode::fast); region.parseOpcode({ "off_time", "0.1" }); REQUIRE(region.offTime == 0.1f); + REQUIRE(region.offMode == SfzOffMode::time); region.parseOpcode({ "off_time", "0" }); REQUIRE(region.offTime == 0.0f); region.parseOpcode({ "off_time", "0.1" }); From c8c60be0dda175ae78f60c85f2e670e9d4e14ce6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 10 Aug 2020 18:30:29 +0200 Subject: [PATCH 092/445] Avoid shadowing an existing variable --- src/sfizz/FilePool.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index ab013a90..0778026e 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -220,9 +220,9 @@ absl::optional sfz::FilePool::getFileInformation(const Fil if (!reader->getInstrument(&instrumentInfo)) { // if no instrument, then try extracting from embedded RIFF chunks (flac) - FileMetadataReader reader; - if (reader.open(file)) - reader.extractRiffInstrument(instrumentInfo); + FileMetadataReader mdReader; + if (mdReader.open(file)) + mdReader.extractRiffInstrument(instrumentInfo); } if (!fileId.isReverse()) { From 23107ee17d329be97c4b1fea04a12b4c7395ac9f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 10 Aug 2020 19:01:30 +0200 Subject: [PATCH 093/445] Rewrite the access function riffChunk in one line --- src/sfizz/FileMetadata.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sfizz/FileMetadata.cpp b/src/sfizz/FileMetadata.cpp index a03069c8..e57f70d5 100644 --- a/src/sfizz/FileMetadata.cpp +++ b/src/sfizz/FileMetadata.cpp @@ -204,8 +204,7 @@ const RiffChunkInfo* FileMetadataReader::riffChunk(size_t index) const const RiffChunkInfo* FileMetadataReader::Impl::riffChunk(size_t index) const { - const std::vector& riffChunks = riffChunks_; - return (index < riffChunks.size()) ? &riffChunks[index] : nullptr; + return (index < riffChunks_.size()) ? &riffChunks_[index] : nullptr; } const RiffChunkInfo* FileMetadataReader::riffChunkById(RiffChunkId id) const From 36f5a62f04209faf3e9e4e7e98a35bfffda1dc86 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 11 Aug 2020 15:31:23 +0200 Subject: [PATCH 094/445] Plugin-level support for host time --- lv2/sfizz.c | 168 +++++++++++++++++++++++++++++++++++- src/sfizz.h | 36 +++++++- src/sfizz.hpp | 32 ++++++- src/sfizz/Synth.cpp | 23 +++++ src/sfizz/Synth.h | 23 +++++ src/sfizz/sfizz.cpp | 19 +++- src/sfizz/sfizz_wrapper.cpp | 15 ++++ vst/SfizzVstProcessor.cpp | 33 ++++++- vst/SfizzVstProcessor.h | 5 ++ 9 files changed, 343 insertions(+), 11 deletions(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 5bafd04b..ef35867c 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -131,7 +131,9 @@ typedef struct LV2_URID sample_rate_uri; LV2_URID atom_object_uri; LV2_URID atom_float_uri; + LV2_URID atom_double_uri; LV2_URID atom_int_uri; + LV2_URID atom_long_uri; LV2_URID atom_urid_uri; LV2_URID atom_path_uri; LV2_URID patch_set_uri; @@ -150,6 +152,12 @@ typedef struct LV2_URID sfizz_check_modification_uri; LV2_URID sfizz_active_voices_uri; LV2_URID time_position_uri; + LV2_URID time_bar_uri; + LV2_URID time_bar_beat_uri; + LV2_URID time_beat_unit_uri; + LV2_URID time_beats_per_bar_uri; + LV2_URID time_beats_per_minute_uri; + LV2_URID time_speed_uri; // Sfizz related data sfizz_synth_t *synth; @@ -166,6 +174,14 @@ typedef struct float sample_rate; atomic_int must_update_midnam; + // Timing data + int bar; + float bar_beat; + int beats_per_bar; + int beat_unit; + float bpm_tempo; + float speed; + // Paths char bundle_path[MAX_BUNDLE_PATH_SIZE]; } sfizz_plugin_t; @@ -187,6 +203,14 @@ enum SFIZZ_ACTIVE_VOICES = 12, }; +enum +{ + SFIZZ_TIMEINFO_POSITION = 1 << 0, + SFIZZ_TIMEINFO_SIGNATURE = 1 << 1, + SFIZZ_TIMEINFO_TEMPO = 1 << 2, + SFIZZ_TIMEINFO_SPEED = 1 << 3, +}; + static void sfizz_lv2_state_free_path(LV2_State_Free_Path_Handle handle, char *path) @@ -210,7 +234,9 @@ sfizz_lv2_map_required_uris(sfizz_plugin_t *self) self->nominal_block_length_uri = map->map(map->handle, LV2_BUF_SIZE__nominalBlockLength); self->sample_rate_uri = map->map(map->handle, LV2_PARAMETERS__sampleRate); self->atom_float_uri = map->map(map->handle, LV2_ATOM__Float); + self->atom_double_uri = map->map(map->handle, LV2_ATOM__Double); self->atom_int_uri = map->map(map->handle, LV2_ATOM__Int); + self->atom_long_uri = map->map(map->handle, LV2_ATOM__Long); self->atom_path_uri = map->map(map->handle, LV2_ATOM__Path); self->atom_urid_uri = map->map(map->handle, LV2_ATOM__URID); self->atom_object_uri = map->map(map->handle, LV2_ATOM__Object); @@ -230,6 +256,68 @@ sfizz_lv2_map_required_uris(sfizz_plugin_t *self) self->sfizz_log_status_uri = map->map(map->handle, SFIZZ__logStatus); self->sfizz_check_modification_uri = map->map(map->handle, SFIZZ__checkModification); self->time_position_uri = map->map(map->handle, LV2_TIME__Position); + self->time_bar_uri = map->map(map->handle, LV2_TIME__bar); + self->time_bar_beat_uri = map->map(map->handle, LV2_TIME__barBeat); + self->time_beat_unit_uri = map->map(map->handle, LV2_TIME__beatUnit); + self->time_beats_per_bar_uri = map->map(map->handle, LV2_TIME__beatsPerBar); + self->time_beats_per_minute_uri = map->map(map->handle, LV2_TIME__beatsPerMinute); + self->time_speed_uri = map->map(map->handle, LV2_TIME__speed); +} + +static bool +sfizz_atom_extract_real(sfizz_plugin_t *self, const LV2_Atom *atom, double *real) +{ + if (!atom) + return false; + + const LV2_URID type = atom->type; + + if (type == self->atom_int_uri && atom->size >= sizeof(int32_t)) { + *real = ((const LV2_Atom_Int *)atom)->body; + return true; + } + if (type == self->atom_long_uri && atom->size >= sizeof(int64_t)) { + *real = ((const LV2_Atom_Long *)atom)->body; + return true; + } + if (type == self->atom_float_uri && atom->size >= sizeof(float)) { + *real = ((const LV2_Atom_Float *)atom)->body; + return true; + } + if (type == self->atom_double_uri && atom->size >= sizeof(double)) { + *real = ((const LV2_Atom_Double *)atom)->body; + return true; + } + + return false; +} + +static bool +sfizz_atom_extract_integer(sfizz_plugin_t *self, const LV2_Atom *atom, int64_t *integer) +{ + if (!atom) + return false; + + const LV2_URID type = atom->type; + + if (type == self->atom_int_uri && atom->size >= sizeof(int32_t)) { + *integer = ((const LV2_Atom_Int *)atom)->body; + return true; + } + if (type == self->atom_long_uri && atom->size >= sizeof(int64_t)) { + *integer = ((const LV2_Atom_Long *)atom)->body; + return true; + } + if (type == self->atom_float_uri && atom->size >= sizeof(float)) { + *integer = (int64_t)((const LV2_Atom_Float *)atom)->body; + return true; + } + if (type == self->atom_double_uri && atom->size >= sizeof(double)) { + *integer = (int64_t)((const LV2_Atom_Double *)atom)->body; + return true; + } + + return false; } static void @@ -326,6 +414,19 @@ sfizz_lv2_get_default_scala_path(LV2_Handle instance, char *path, size_t size) snprintf(path, size, "%s/%s", self->bundle_path, DEFAULT_SCALA_FILE); } +static void +sfizz_lv2_update_timeinfo(sfizz_plugin_t *self, int delay, int updates) +{ + if (updates & SFIZZ_TIMEINFO_POSITION) + sfizz_send_time_position(self->synth, delay, self->bar, self->bar_beat); + if (updates & SFIZZ_TIMEINFO_SIGNATURE) + sfizz_send_time_signature(self->synth, delay, self->beats_per_bar, self->beat_unit); + if (updates & SFIZZ_TIMEINFO_TEMPO) + sfizz_send_tempo(self->synth, delay, 60.0f / self->bpm_tempo); + if (updates & SFIZZ_TIMEINFO_SPEED) + sfizz_send_playback_state(self->synth, delay, self->speed > 0); +} + static LV2_Handle instantiate(const LV2_Descriptor *descriptor, double rate, @@ -361,6 +462,14 @@ instantiate(const LV2_Descriptor *descriptor, self->check_modification = false; self->sample_counter = 0; + // Initial timing + self->bar = 0; + self->bar_beat = 0; + self->beats_per_bar = 4; + self->beat_unit = 4; + self->bpm_tempo = 120; + self->speed = 1; + // Get the features from the host and populate the structure for (const LV2_Feature *const *f = features; *f; f++) { @@ -472,6 +581,8 @@ instantiate(const LV2_Descriptor *descriptor, sfizz_load_file(self->synth, self->sfz_file_path); sfizz_load_scala_file(self->synth, self->scala_file_path); + sfizz_lv2_update_timeinfo(self, 0, ~0); + return (LV2_Handle)self; } @@ -751,6 +862,8 @@ run(LV2_Handle instance, uint32_t sample_count) LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev) { + const int delay = (int)ev->time.frames; + // If the received atom is an object/patch message if (ev->body.type == self->atom_object_uri) { @@ -779,7 +892,60 @@ run(LV2_Handle instance, uint32_t sample_count) } else if (obj->body.otype == self->time_position_uri) { - // TODO: Handle time position atom + const LV2_Atom *bar_atom = NULL; + const LV2_Atom *bar_beat_atom = NULL; + const LV2_Atom *beat_unit_atom = NULL; + const LV2_Atom *beats_per_bar_atom = NULL; + const LV2_Atom *beats_per_minute_atom = NULL; + const LV2_Atom *speed_atom = NULL; + + lv2_atom_object_get( + obj, + self->time_bar_uri, &bar_atom, + self->time_bar_beat_uri, &bar_beat_atom, + self->time_beats_per_bar_uri, &beats_per_bar_atom, + self->time_beats_per_minute_uri, &beats_per_minute_atom, + self->time_beat_unit_uri, &beat_unit_atom, + self->time_speed_uri, &speed_atom, + 0); + + int updates = 0; + + int64_t bar; + double bar_beat; + if (sfizz_atom_extract_integer(self, bar_atom, &bar)) { + self->bar = (int)bar; + updates |= SFIZZ_TIMEINFO_POSITION; + } + if (sfizz_atom_extract_real(self, bar_beat_atom, &bar_beat)) { + self->bar_beat = (float)bar_beat; + updates |= SFIZZ_TIMEINFO_POSITION; + } + + double beats_per_bar; + int64_t beat_unit; + if (sfizz_atom_extract_real(self, beats_per_bar_atom, &beats_per_bar)) { + self->beats_per_bar = (int)beats_per_bar; + updates |= SFIZZ_TIMEINFO_SIGNATURE; + } + if (sfizz_atom_extract_integer(self, beat_unit_atom, &beat_unit)) { + self->beat_unit = (int)beat_unit; + updates |= SFIZZ_TIMEINFO_SIGNATURE; + } + + double tempo; + if (sfizz_atom_extract_real(self, beats_per_minute_atom, &tempo)) { + self->bpm_tempo = (float)tempo; + updates |= SFIZZ_TIMEINFO_TEMPO; + } + + double speed; + if (sfizz_atom_extract_real(self, speed_atom, &speed)) { + self->speed = (float)speed; + updates |= SFIZZ_TIMEINFO_SPEED; + } + + sfizz_lv2_update_timeinfo(self, delay, updates); } else { diff --git a/src/sfizz.h b/src/sfizz.h index 05034e7d..5c8f7d1a 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -302,13 +302,43 @@ SFIZZ_EXPORTED_API void sfizz_send_pitch_wheel(sfizz_synth_t* synth, int delay, SFIZZ_EXPORTED_API void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, char aftertouch); /** - * @brief Send a tempo event. (CURRENTLY UNIMPLEMENTED) + * @brief Send a tempo event. * * @param synth The synth. * @param delay The delay. - * @param seconds_per_quarter The seconds per quarter. + * @param seconds_per_beat The seconds per beat. */ -SFIZZ_EXPORTED_API void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float seconds_per_quarter); +SFIZZ_EXPORTED_API void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float seconds_per_beat); + +/** + * @brief Send the time signature. + * + * @param synth The synth. + * @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. + */ +SFIZZ_EXPORTED_API void sfizz_send_time_signature(sfizz_synth_t* synth, int delay, int beats_per_bar, int beat_unit); + +/** + * @brief Send the time position. + * + * @param synth The synth. + * @param delay The delay. + * @param bar The current bar. + * @param bar_beat The fractional position of the current beat within the bar. + */ +SFIZZ_EXPORTED_API void sfizz_send_time_position(sfizz_synth_t* synth, int delay, int bar, float bar_beat); + +/** + * @brief Send the playback state. + * + * @param synth The synth. + * @param delay The delay. + * @param playback_state The playback state, 1 if playing, 0 if stopped. + */ +SFIZZ_EXPORTED_API void sfizz_send_playback_state(sfizz_synth_t* synth, int delay, int playback_state); + /** * @brief Render a block audio data into a stereo channel. No other channel diff --git a/src/sfizz.hpp b/src/sfizz.hpp index 40d9f424..957c7d41 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -291,13 +291,39 @@ public: void aftertouch(int delay, uint8_t aftertouch) noexcept; /** - * @brief Send a tempo event to the synth. (CURRENTLY UNIMPLEMENTED) + * @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 secondsPerQuarter the new period of the quarter note. + * @param secondsPerBeat the new period of the beat. */ - void tempo(int delay, float secondsPerQuarter) noexcept; + void tempo(int delay, float secondsPerBeat) noexcept; + + /** + * @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. + */ + void timeSignature(int delay, int beatsPerBar, int beatUnit); + + /** + * @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. + */ + void timePosition(int delay, int bar, float barBeat); + + /** + * @brief Send the playback state. + * + * @param delay The delay. + * @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 diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index afea3140..4eaff991 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1116,6 +1116,29 @@ void sfz::Synth::tempo(int /* delay */, float /* secondsPerQuarter */) noexcept { ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; } +void sfz::Synth::timeSignature(int delay, int beatsPerBar, int beatUnit) +{ + ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; + + (void)delay; + (void)beatsPerBar; + (void)beatUnit; +} +void sfz::Synth::timePosition(int delay, int bar, float barBeat) +{ + ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; + + (void)delay; + (void)bar; + (void)barBeat; +} +void sfz::Synth::playbackState(int delay, int playbackState) +{ + ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; + + (void)delay; + (void)playbackState; +} int sfz::Synth::getNumRegions() const noexcept { diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3b7ce3b8..30ace997 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -392,6 +392,29 @@ public: * @param secondsPerQuarter the new period of the quarter note */ void tempo(int delay, float secondsPerQuarter) noexcept; + /** + * @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. + */ + void timeSignature(int delay, int beatsPerBar, int beatUnit); + /** + * @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. + */ + void timePosition(int delay, int bar, float barBeat); + /** + * @brief Send the playback state. + * + * @param delay The delay. + * @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 diff --git a/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index fbdd041c..13cfd8a9 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -153,9 +153,24 @@ void sfz::Sfizz::aftertouch(int delay, uint8_t aftertouch) noexcept synth->aftertouch(delay, aftertouch); } -void sfz::Sfizz::tempo(int delay, float secondsPerQuarter) noexcept +void sfz::Sfizz::tempo(int delay, float secondsPerBeat) noexcept { - synth->tempo(delay, secondsPerQuarter); + synth->tempo(delay, secondsPerBeat); +} + +void sfz::Sfizz::timeSignature(int delay, int beatsPerBar, int beatUnit) +{ + synth->timeSignature(delay, beatsPerBar, beatUnit); +} + +void sfz::Sfizz::timePosition(int delay, int bar, float barBeat) +{ + synth->timePosition(delay, bar, barBeat); +} + +void sfz::Sfizz::playbackState(int delay, int playbackState) +{ + synth->playbackState(delay, playbackState); } void sfz::Sfizz::renderBlock(float** buffers, size_t numSamples, int /*numOutputs*/) noexcept diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index de33d815..3e103b2e 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -160,6 +160,21 @@ void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float seconds_per_quarter auto self = reinterpret_cast(synth); self->tempo(delay, seconds_per_quarter); } +void sfizz_send_time_signature(sfizz_synth_t* synth, int delay, int beats_per_bar, int beat_unit) +{ + auto self = reinterpret_cast(synth); + self->timeSignature(delay, beats_per_bar, beat_unit); +} +void sfizz_send_time_position(sfizz_synth_t* synth, int delay, int bar, float bar_beat) +{ + auto self = reinterpret_cast(synth); + self->timePosition(delay, bar, bar_beat); +} +void sfizz_send_playback_state(sfizz_synth_t* synth, int delay, int playback_state) +{ + auto self = reinterpret_cast(synth); + self->playbackState(delay, playback_state); +} void sfizz_render_block(sfizz_synth_t* synth, float** channels, int num_channels, int num_frames) { diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index 56d951e3..e26fd23d 100644 --- a/vst/SfizzVstProcessor.cpp +++ b/vst/SfizzVstProcessor.cpp @@ -12,8 +12,6 @@ #include "pluginterfaces/vst/ivstparameterchanges.h" #include -#pragma message("TODO: send tempo") // NOLINT - template constexpr int fastRound(T x) { @@ -55,6 +53,13 @@ tresult PLUGIN_API SfizzVstProcessor::initialize(FUnknown* context) _currentStretchedTuning = 0.0; loadSfzFileOrDefault(*_synth, {}); + _synth->tempo(0, 0.5); + _timeSigNumerator = 4; + _timeSigDenominator = 4; + _synth->timeSignature(0, _timeSigNumerator, _timeSigDenominator); + _synth->timePosition(0, 0, 0); + _synth->playbackState(0, 0); + return result; } @@ -143,6 +148,9 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) { sfz::Sfizz& synth = *_synth; + if (data.processContext) + updateTimeInfo(*data.processContext); + if (Vst::IParameterChanges* pc = data.inputParameterChanges) processParameterChanges(*pc); @@ -198,6 +206,27 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) return kResultTrue; } +void SfizzVstProcessor::updateTimeInfo(const Vst::ProcessContext& context) +{ + sfz::Sfizz& synth = *_synth; + + if (context.state & context.kTempoValid) + synth.tempo(0, 60.0f / context.tempo); + + if (context.state & context.kTimeSigValid) { + _timeSigNumerator = context.timeSigNumerator; + _timeSigDenominator = context.timeSigDenominator; + synth.timeSignature(0, _timeSigNumerator, _timeSigDenominator); + } + + if (context.state & context.kProjectTimeMusicValid) { + double beats = context.projectTimeMusic * 0.25 * _timeSigDenominator; + double bars = beats / _timeSigNumerator; + beats -= int(bars) * _timeSigNumerator; + synth.timePosition(0, int(bars), float(beats)); + } +} + void SfizzVstProcessor::processParameterChanges(Vst::IParameterChanges& pc) { uint32 paramCount = pc.getParameterCount(); diff --git a/vst/SfizzVstProcessor.h b/vst/SfizzVstProcessor.h index f215baf2..76af84a6 100644 --- a/vst/SfizzVstProcessor.h +++ b/vst/SfizzVstProcessor.h @@ -64,6 +64,11 @@ private: uint32 _fileChangeCounter = 0; uint32 _fileChangePeriod = 0; + // time info + int _timeSigNumerator = 0; + int _timeSigDenominator = 0; + void updateTimeInfo(const Vst::ProcessContext& context); + // messaging struct RTMessage { const char* type; From 95e730f5b1dd883b4a5f7584ef5d9a6709959b56 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 11 Aug 2020 16:20:55 +0200 Subject: [PATCH 095/445] Add the playback state on VST --- vst/SfizzVstProcessor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index e26fd23d..e64e184f 100644 --- a/vst/SfizzVstProcessor.cpp +++ b/vst/SfizzVstProcessor.cpp @@ -225,6 +225,8 @@ void SfizzVstProcessor::updateTimeInfo(const Vst::ProcessContext& context) beats -= int(bars) * _timeSigNumerator; synth.timePosition(0, int(bars), float(beats)); } + + synth.playbackState(0, (context.state & context.kPlaying) != 0); } void SfizzVstProcessor::processParameterChanges(Vst::IParameterChanges& pc) From bf8b3fd11166e717708587ce0bbca7cc305d4907 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 11 Aug 2020 17:06:09 +0200 Subject: [PATCH 096/445] Support multi-level volumes --- src/sfizz/Region.cpp | 32 +++++++++++++++++++++++++++++++- src/sfizz/Region.h | 7 +++++++ tests/RegionT.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index c8aee196..4487354b 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -539,6 +539,27 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("rt_decay"): setValueFromOpcode(opcode, rtDecay, Default::rtDecayRange); break; + case hash("global_amplitude"): + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) + globalAmplitude = normalizePercents(*value); + break; + case hash("master_amplitude"): + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) + masterAmplitude = normalizePercents(*value); + break; + case hash("group_amplitude"): + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) + groupAmplitude = normalizePercents(*value); + break; + case hash("global_volume"): + setValueFromOpcode(opcode, globalVolume, Default::volumeRange); + break; + case hash("master_volume"): + setValueFromOpcode(opcode, masterVolume, Default::volumeRange); + break; + case hash("group_volume"): + setValueFromOpcode(opcode, groupVolume, Default::volumeRange); + break; // Performance parameters: filters case hash("cutoff&"): // also cutoff @@ -1165,6 +1186,9 @@ float sfz::Region::getBaseVolumedB(int noteNumber) const noexcept { fast_real_distribution volumeDistribution { -ampRandom, ampRandom }; auto baseVolumedB = volume + volumeDistribution(Random::randomGenerator); + baseVolumedB += globalVolume; + baseVolumedB += masterVolume; + baseVolumedB += groupVolume; if (trigger == SfzTrigger::release || trigger == SfzTrigger::release_key) baseVolumedB -= rtDecay * midiState.getNoteDuration(noteNumber); return baseVolumedB; @@ -1172,7 +1196,13 @@ float sfz::Region::getBaseVolumedB(int noteNumber) const noexcept float sfz::Region::getBaseGain() const noexcept { - return amplitude; + float baseGain = amplitude; + + baseGain *= globalAmplitude; + baseGain *= masterAmplitude; + baseGain *= groupAmplitude; + + return baseGain; } float sfz::Region::getPhase() const noexcept diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 72c29ad9..9376d236 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -345,6 +345,13 @@ struct Region { CCMap> crossfadeCCOutRange { Default::crossfadeCCOutRange }; // xfout_loccN xfout_hiccN float rtDecay { Default::rtDecay }; // rt_decay + float globalAmplitude { 1.0 }; // global_amplitude + float masterAmplitude { 1.0 }; // master_amplitude + float groupAmplitude { 1.0 }; // group_amplitude + float globalVolume { 0.0 }; // global_volume + float masterVolume { 0.0 }; // master_volume + float groupVolume { 0.0 }; // group_volume + // Filters and EQs std::vector equalizers; std::vector filters; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 481e8628..051a4fe5 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -882,6 +882,46 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.crossfadeCCCurve == SfzCrossfadeCurve::gain); } + SECTION("*_volume") + { + const std::pair assoc_pairs[] = { + {"global_volume", ®ion.globalVolume}, + {"master_volume", ®ion.masterVolume}, + {"group_volume", ®ion.groupVolume}, + }; + for (auto a : assoc_pairs) { + REQUIRE(region.volume == 0.0f); + region.parseOpcode({ a.first, "4.2" }); + REQUIRE(*a.second == 4.2f); + region.parseOpcode({ a.first, "-4.2" }); + REQUIRE(*a.second == -4.2f); + region.parseOpcode({ a.first, "-123" }); + REQUIRE(*a.second == -123.0f); + region.parseOpcode({ a.first, "-185" }); + REQUIRE(*a.second == -144.0f); + region.parseOpcode({ a.first, "79" }); + REQUIRE(*a.second == 48.0f); + } + } + + SECTION("*_amplitude") + { + const std::pair assoc_pairs[] = { + {"global_amplitude", ®ion.globalAmplitude}, + {"master_amplitude", ®ion.masterAmplitude}, + {"group_amplitude", ®ion.groupAmplitude}, + }; + for (auto a : assoc_pairs) { + REQUIRE(*a.second == 1.0_a); + region.parseOpcode({ a.first, "40" }); + REQUIRE(*a.second == 0.4_a); + region.parseOpcode({ a.first, "-40" }); + REQUIRE(*a.second == 0_a); + region.parseOpcode({ a.first, "140" }); + REQUIRE(*a.second == 1.0_a); + } + } + SECTION("pitch_keycenter") { REQUIRE(region.pitchKeycenter == 60); From b1fef10d0191adedbbaef0b37416493091c04846 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 11 Aug 2020 22:30:33 +0200 Subject: [PATCH 097/445] Const stuff --- src/sfizz/LFO.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/sfizz/LFO.cpp b/src/sfizz/LFO.cpp index 6d1da07d..cb7f0747 100644 --- a/src/sfizz/LFO.cpp +++ b/src/sfizz/LFO.cpp @@ -127,12 +127,12 @@ void LFO::processWave(unsigned nth, absl::Span out) const LFODescription::Sub& sub = desc.sub[nth]; const size_t numFrames = out.size(); - float samplePeriod = 1.0f / impl.sampleRate_; + const float samplePeriod = 1.0f / impl.sampleRate_; + const float baseFreq = desc.freq; + const float offset = sub.offset; + const float ratio = sub.ratio; + const float scale = sub.scale; float phase = impl.subPhases_[nth]; - float baseFreq = desc.freq; - float offset = sub.offset; - float ratio = sub.ratio; - float scale = sub.scale; for (size_t i = 0; i < numFrames; ++i) { out[i] += offset + scale * eval(phase); @@ -156,13 +156,13 @@ void LFO::processSH(unsigned nth, absl::Span out) const LFODescription::Sub& sub = desc.sub[nth]; const size_t numFrames = out.size(); - float samplePeriod = 1.0f / impl.sampleRate_; - float phase = impl.subPhases_[nth]; - float baseFreq = desc.freq; - float offset = sub.offset; - float ratio = sub.ratio; - float scale = sub.scale; + const float samplePeriod = 1.0f / impl.sampleRate_; + const float baseFreq = desc.freq; + const float offset = sub.offset; + const float ratio = sub.ratio; + const float scale = sub.scale; float sampleHoldValue = impl.sampleHoldMem_[nth]; + float phase = impl.subPhases_[nth]; for (size_t i = 0; i < numFrames; ++i) { out[i] += offset + scale * sampleHoldValue; @@ -203,12 +203,12 @@ void LFO::processSteps(absl::Span out) if (numSteps <= 0) return; - float samplePeriod = 1.0f / impl.sampleRate_; + const float samplePeriod = 1.0f / impl.sampleRate_; + const float baseFreq = desc.freq; + const float offset = sub.offset; + const float ratio = sub.ratio; + const float scale = sub.scale; float phase = impl.subPhases_[nth]; - float baseFreq = desc.freq; - float offset = sub.offset; - float ratio = sub.ratio; - float scale = sub.scale; for (size_t i = 0; i < numFrames; ++i) { float step = steps[static_cast(phase * numSteps)]; From e70cadc8cc209c48a5bfd6268dc12476bd3aca9e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 11 Aug 2020 22:48:18 +0200 Subject: [PATCH 098/445] Typo --- src/sfizz/LFO.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/LFO.h b/src/sfizz/LFO.h index 86d1f0f3..fbd9dd14 100644 --- a/src/sfizz/LFO.h +++ b/src/sfizz/LFO.h @@ -39,7 +39,7 @@ struct LFODescription; note: if there are gaps in subwaveforms, these subwaveforms which are gaps will be initialized and processed. - example: lfo1_ratio4=1.0 // instanciate implicitly the subs #2 and #3 + example: lfo1_ratio4=1.0 // instantiate implicitly the subs #2 and #3 lfoN_wave[X]: Wave lfoN_offset[X]: DC offset - Add to LFO output; not affected by scale. From ab868df3c45a22e9ff3661e92f80ae5a765bc3d6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 11 Aug 2020 22:52:17 +0200 Subject: [PATCH 099/445] Const stuff --- src/sfizz/Region.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 95619178..83086730 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -941,8 +941,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (lfoNumber == 0) return false; if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) { - ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); - ModKey target = ModKey::createNXYZ(ModId::Amplitude, id); + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Amplitude, id); getOrCreateConnection(source, target).sourceDepth = *value; } } @@ -953,8 +953,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (lfoNumber == 0) return false; if (auto value = readOpcode(opcode.value, Default::panCCRange)) { - ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); - ModKey target = ModKey::createNXYZ(ModId::Pan, id); + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Pan, id); getOrCreateConnection(source, target).sourceDepth = *value; } } @@ -965,8 +965,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (lfoNumber == 0) return false; if (auto value = readOpcode(opcode.value, Default::widthCCRange)) { - ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); - ModKey target = ModKey::createNXYZ(ModId::Width, id); + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Width, id); getOrCreateConnection(source, target).sourceDepth = *value; } } @@ -977,8 +977,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (lfoNumber == 0) return false; if (auto value = readOpcode(opcode.value, Default::positionCCRange)) { - ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); - ModKey target = ModKey::createNXYZ(ModId::Position, id); + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Position, id); getOrCreateConnection(source, target).sourceDepth = *value; } } @@ -989,8 +989,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (lfoNumber == 0) return false; if (auto value = readOpcode(opcode.value, Default::tuneCCRange)) { - ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); - ModKey target = ModKey::createNXYZ(ModId::Pitch, id); + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Pitch, id); getOrCreateConnection(source, target).sourceDepth = *value; } } @@ -1001,8 +1001,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (lfoNumber == 0) return false; if (auto value = readOpcode(opcode.value, Default::volumeCCRange)) { - ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); - ModKey target = ModKey::createNXYZ(ModId::Volume, id); + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Volume, id); getOrCreateConnection(source, target).sourceDepth = *value; } } From 88ea89c5fbbe8cbaa3420a4b212a48c8e71db682 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 11 Aug 2020 23:46:08 +0200 Subject: [PATCH 100/445] Initialize the LFOs in sfizz_plot_lfo --- tests/PlotLFO.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PlotLFO.cpp b/tests/PlotLFO.cpp index c8f63e2f..d993236a 100644 --- a/tests/PlotLFO.cpp +++ b/tests/PlotLFO.cpp @@ -83,6 +83,10 @@ int main(int argc, char* argv[]) size_t numFrames = (size_t)std::ceil(sampleRate * duration); std::vector outputMemory(numLfos * numFrames); + for (size_t l = 0; l < numLfos; ++l) { + lfos[l].start(); + } + std::vector> lfoOutputs(numLfos); for (size_t l = 0; l < numLfos; ++l) { lfoOutputs[l] = absl::MakeSpan(&outputMemory[l * numFrames], numFrames); From f94e868155191dc44e1d0f507515273f82a1439a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 12 Aug 2020 07:46:54 +0200 Subject: [PATCH 101/445] Elimination of some warnings --- src/sfizz/effects/gen/compressor.cxx | 1 + src/sfizz/effects/gen/disto_stage.cxx | 1 + src/sfizz/effects/gen/fverb.cxx | 1 + src/sfizz/effects/gen/gate.cxx | 1 + 4 files changed, 4 insertions(+) diff --git a/src/sfizz/effects/gen/compressor.cxx b/src/sfizz/effects/gen/compressor.cxx index 6634dab0..ba19b870 100644 --- a/src/sfizz/effects/gen/compressor.cxx +++ b/src/sfizz/effects/gen/compressor.cxx @@ -82,6 +82,7 @@ class faustCompressor { } static void classInit(int sample_rate) { + (void)sample_rate; } void instanceConstants(int sample_rate) { diff --git a/src/sfizz/effects/gen/disto_stage.cxx b/src/sfizz/effects/gen/disto_stage.cxx index 7353b7ed..e630bd68 100644 --- a/src/sfizz/effects/gen/disto_stage.cxx +++ b/src/sfizz/effects/gen/disto_stage.cxx @@ -55,6 +55,7 @@ class faustDistoSIG0 { } void instanceInitfaustDistoSIG0(int sample_rate) { + (void)sample_rate; for (int l3 = 0; (l3 < 2); l3 = (l3 + 1)) { iRec3[l3] = 0; } diff --git a/src/sfizz/effects/gen/fverb.cxx b/src/sfizz/effects/gen/fverb.cxx index 07a742d8..849c2485 100644 --- a/src/sfizz/effects/gen/fverb.cxx +++ b/src/sfizz/effects/gen/fverb.cxx @@ -58,6 +58,7 @@ class faustFverbSIG0 { } void instanceInitfaustFverbSIG0(int sample_rate) { + (void)sample_rate; for (int l4 = 0; (l4 < 2); l4 = (l4 + 1)) { iRec19[l4] = 0; } diff --git a/src/sfizz/effects/gen/gate.cxx b/src/sfizz/effects/gen/gate.cxx index 099eb3c5..93da987b 100644 --- a/src/sfizz/effects/gen/gate.cxx +++ b/src/sfizz/effects/gen/gate.cxx @@ -85,6 +85,7 @@ class faustGate { } static void classInit(int sample_rate) { + (void)sample_rate; } void instanceConstants(int sample_rate) { From ff8ac64d61ac846d89f975d7af88b89c7db064a8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 12 Aug 2020 16:12:04 +0200 Subject: [PATCH 102/445] Add clamping SIMD helper --- benchmarks/BM_clamp.cpp | 52 ++++++++++++++++++++++++++++++++++ benchmarks/CMakeLists.txt | 1 + src/sfizz/SIMDHelpers.cpp | 10 +++++++ src/sfizz/SIMDHelpers.h | 26 +++++++++++++++++ src/sfizz/simd/HelpersSSE.cpp | 31 ++++++++++++++++++++ src/sfizz/simd/HelpersSSE.h | 1 + src/sfizz/simd/HelpersScalar.h | 14 +++++++++ tests/SIMDHelpersT.cpp | 27 ++++++++++++++++++ 8 files changed, 162 insertions(+) create mode 100644 benchmarks/BM_clamp.cpp diff --git a/benchmarks/BM_clamp.cpp b/benchmarks/BM_clamp.cpp new file mode 100644 index 00000000..beb66ae5 --- /dev/null +++ b/benchmarks/BM_clamp.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SIMDHelpers.h" +#include "Macros.h" +#include +#include +#include +#include +#include +#include + +class ClampArray : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) { + std::random_device rd { }; + std::mt19937 gen { rd() }; + std::uniform_real_distribution dist { 0, 10 }; + input = std::vector(state.range(0)); + std::generate(input.begin(), input.end(), [&]() { return dist(gen); }); + } + + void TearDown(const ::benchmark::State& state) { + UNUSED(state); + } + + std::vector input; +}; + +BENCHMARK_DEFINE_F(ClampArray, Scalar)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::clampAll, false); + sfz::clampAll(absl::MakeSpan(input), 1.2f, 3.8f); + } +} + +BENCHMARK_DEFINE_F(ClampArray, SIMD)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::clampAll, true); + sfz::clampAll(absl::MakeSpan(input), 1.2f, 3.8f); + } +} + + +BENCHMARK_REGISTER_F(ClampArray, Scalar)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(ClampArray, SIMD)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_MAIN(); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index cb38feb6..0c6e981e 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -59,6 +59,7 @@ sfizz_add_benchmark(bm_maps BM_maps.cpp) target_link_libraries(bm_maps PRIVATE absl::flat_hash_map) sfizz_add_benchmark(bm_mapVsArray BM_mapVsArray.cpp) sfizz_add_benchmark(bm_random BM_random.cpp) +sfizz_add_benchmark(bm_clamp BM_clamp.cpp) sfizz_add_benchmark(bm_logger BM_logger.cpp) target_link_libraries(bm_logger PRIVATE sfizz::sfizz) diff --git a/src/sfizz/SIMDHelpers.cpp b/src/sfizz/SIMDHelpers.cpp index c0e21d4e..c631a0ca 100644 --- a/src/sfizz/SIMDHelpers.cpp +++ b/src/sfizz/SIMDHelpers.cpp @@ -40,6 +40,7 @@ struct SIMDDispatch { decltype(&diffScalar) diff = &diffScalar; decltype(&meanScalar) mean = &meanScalar; decltype(&meanSquaredScalar) meanSquared = &meanSquaredScalar; + decltype(&clampAllScalar) clampAll = &clampAllScalar; private: std::array(SIMDOps::_sentinel)> simdStatus; @@ -84,6 +85,7 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(diff) SIMD_OP(mean) SIMD_OP(meanSquared) + SIMD_OP(clampAll) } #undef SIMD_OP } @@ -119,6 +121,7 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(diff) SIMD_OP(mean) SIMD_OP(meanSquared) + SIMD_OP(clampAll) } } #undef SIMD_OP @@ -159,6 +162,7 @@ void SIMDDispatch::resetStatus() setStatus(SIMDOps::mean, false); setStatus(SIMDOps::meanSquared, false); setStatus(SIMDOps::upsampling, true); + setStatus(SIMDOps::clampAll, false); } /// @@ -301,4 +305,10 @@ void diff(const float* input, float* output, unsigned size) noexcept return simdDispatch().diff(input, output, size); } +template <> +void clampAll(float* input, float low, float high, unsigned size) noexcept +{ + return simdDispatch().clampAll(input, low, high, size); +} + } diff --git a/src/sfizz/SIMDHelpers.h b/src/sfizz/SIMDHelpers.h index ec27824a..17b21c88 100644 --- a/src/sfizz/SIMDHelpers.h +++ b/src/sfizz/SIMDHelpers.h @@ -59,6 +59,7 @@ enum class SIMDOps { mean, meanSquared, upsampling, + clampAll, _sentinel // }; @@ -622,4 +623,29 @@ void diff(absl::Span input, absl::Span output) noexcept diff(input.data(), output.data(), minSpanSize(input, output)); } +/** + * @brief Clamp a vector between a low and high bound + * + * @tparam T the underlying type + * @param input + * @param output + * @param low + * @param high + * @param size + */ +template +void clampAll(T* input, T low, T high, unsigned size) noexcept +{ + clampAllScalar(input, low, high, size); +} + +template <> +void clampAll(float* input, float low, float high, unsigned size) noexcept; + +template +void clampAll(absl::Span input, T low, T high) noexcept +{ + clampAll(input.data(), low, high, input.size()); +} + } // namespace sfz diff --git a/src/sfizz/simd/HelpersSSE.cpp b/src/sfizz/simd/HelpersSSE.cpp index 079d6a75..7a0e7beb 100644 --- a/src/sfizz/simd/HelpersSSE.cpp +++ b/src/sfizz/simd/HelpersSSE.cpp @@ -472,3 +472,34 @@ void diffSSE(const float* input, float* output, unsigned size) noexcept incrementAll(input, output); } } + +void clampAllSSE(float* input, float low, float high, unsigned size) noexcept +{ + if (size == 0) + return; + + const auto sentinel = input + size; + +#if SFIZZ_HAVE_SSE2 + const auto* lastAligned = prevAligned(sentinel); + while (unaligned(input) && input < lastAligned){ + const float clampedAbove = *input > high ? high : *input; + *input = clampedAbove < low ? low : clampedAbove; + incrementAll(input); + } + + const auto mmLow = _mm_set1_ps(low); + const auto mmHigh = _mm_set1_ps(high); + while (input < lastAligned) { + const auto mmIn = _mm_load_ps(input); + _mm_store_ps(input, _mm_max_ps(_mm_min_ps(mmIn, mmHigh), mmLow)); + incrementAll(input); + } +#endif + + while (input < sentinel) { + const float clampedAbove = *input > high ? high : *input; + *input = clampedAbove < low ? low : clampedAbove; + incrementAll(input); + } +} diff --git a/src/sfizz/simd/HelpersSSE.h b/src/sfizz/simd/HelpersSSE.h index a6d5e0a5..7100ee89 100644 --- a/src/sfizz/simd/HelpersSSE.h +++ b/src/sfizz/simd/HelpersSSE.h @@ -25,3 +25,4 @@ float meanSSE(const float* vector, unsigned size) noexcept; float meanSquaredSSE(const float* vector, unsigned size) noexcept; void cumsumSSE(const float* input, float* output, unsigned size) noexcept; void diffSSE(const float* input, float* output, unsigned size) noexcept; +void clampAllSSE(float* input, float low, float high, unsigned size) noexcept; diff --git a/src/sfizz/simd/HelpersScalar.h b/src/sfizz/simd/HelpersScalar.h index ab1415a7..aa2017d1 100644 --- a/src/sfizz/simd/HelpersScalar.h +++ b/src/sfizz/simd/HelpersScalar.h @@ -186,3 +186,17 @@ void diffScalar(const T* input, T* output, unsigned size) noexcept incrementAll(input, output); } } + +template +void clampAllScalar(T* input, T low, T high, unsigned size ) noexcept +{ + if (size == 0) + return; + + const auto sentinel = input + size; + while (input < sentinel) { + const float clampedAbove = *input > high ? high : *input; + *input = clampedAbove < low ? low : clampedAbove; + incrementAll(input); + } +} diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index 9a7c9175..ebc94feb 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -812,3 +812,30 @@ TEST_CASE("[Helpers] Width Scalar") REQUIRE(right[0] == Approx(1.0f).margin(0.001f)); } } + +TEST_CASE("[Helpers] clampAll") +{ + std::array inputScalar { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f }; + std::array inputSIMD; + sfz::copy(inputScalar, absl::MakeSpan(inputSIMD)); + std::array expected { 2.5f, 2.5f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 8.0f, 8.0f }; + sfz::setSIMDOpStatus(sfz::SIMDOps::clampAll, false); + sfz::clampAll(absl::MakeSpan(inputScalar), 2.5f, 8.0f); + REQUIRE( approxEqual(inputScalar, expected) ); + sfz::setSIMDOpStatus(sfz::SIMDOps::clampAll, true); + sfz::clampAll(absl::MakeSpan(inputSIMD), 2.5f, 8.0f); + REQUIRE( approxEqual(inputSIMD, expected) ); +} + +TEST_CASE("[Helpers] clampAll (SIMD vs scalar)") +{ + std::vector inputScalar(medBufferSize); + std::vector inputSIMD(medBufferSize); + absl::c_iota(inputScalar, 2.0f); + sfz::copy(inputScalar, absl::MakeSpan(inputSIMD)); + sfz::setSIMDOpStatus(sfz::SIMDOps::clampAll, false); + sfz::clampAll(absl::MakeSpan(inputScalar), 10.0f, 50.0f); + sfz::setSIMDOpStatus(sfz::SIMDOps::clampAll, true); + sfz::clampAll(absl::MakeSpan(inputSIMD), 10.0f, 50.0f); + REQUIRE( approxEqual(inputScalar, inputSIMD) ); +} From c034fd3e304d36ab28b7208b7f0fa404b51f8284 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 12 Aug 2020 16:54:49 +0200 Subject: [PATCH 103/445] Add an allWithin SIMD helper --- benchmarks/BM_allWithin.cpp | 70 ++++++++++++++++++++++++++++++++++ benchmarks/CMakeLists.txt | 1 + src/sfizz/SIMDHelpers.cpp | 12 +++++- src/sfizz/SIMDHelpers.h | 26 ++++++++++++- src/sfizz/simd/HelpersSSE.cpp | 41 ++++++++++++++++++++ src/sfizz/simd/HelpersSSE.h | 1 + src/sfizz/simd/HelpersScalar.h | 20 ++++++++++ tests/SIMDHelpersT.cpp | 11 ++++++ 8 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 benchmarks/BM_allWithin.cpp diff --git a/benchmarks/BM_allWithin.cpp b/benchmarks/BM_allWithin.cpp new file mode 100644 index 00000000..c1b3bec0 --- /dev/null +++ b/benchmarks/BM_allWithin.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SIMDHelpers.h" +#include "Macros.h" +#include +#include +#include +#include +#include +#include + +class WithinArray : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) { + std::random_device rd { }; + std::mt19937 gen { rd() }; + std::uniform_real_distribution dist { 1, 10 }; + input = std::vector(state.range(0)); + std::generate(input.begin(), input.end(), [&]() { return dist(gen); }); + } + + void TearDown(const ::benchmark::State& state) { + UNUSED(state); + } + + std::vector input; +}; + +BENCHMARK_DEFINE_F(WithinArray, ScalarFalse)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, false); + sfz::allWithin(input, 1.2f, 3.8f); + } +} + +BENCHMARK_DEFINE_F(WithinArray, SIMDFalse)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, true); + sfz::allWithin(input, 1.2f, 3.8f); + } +} + +BENCHMARK_DEFINE_F(WithinArray, ScalarTrue)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, false); + sfz::allWithin(input, 0.0f, 11.0f); + } +} + +BENCHMARK_DEFINE_F(WithinArray, SIMDTrue)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, true); + sfz::allWithin(input, 0.0f, 11.0f); + } +} + + +BENCHMARK_REGISTER_F(WithinArray, ScalarFalse)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(WithinArray, SIMDFalse)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(WithinArray, ScalarTrue)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(WithinArray, SIMDTrue)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_MAIN(); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 0c6e981e..71fd0b14 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -60,6 +60,7 @@ target_link_libraries(bm_maps PRIVATE absl::flat_hash_map) sfizz_add_benchmark(bm_mapVsArray BM_mapVsArray.cpp) sfizz_add_benchmark(bm_random BM_random.cpp) sfizz_add_benchmark(bm_clamp BM_clamp.cpp) +sfizz_add_benchmark(bm_allWithin BM_allWithin.cpp) sfizz_add_benchmark(bm_logger BM_logger.cpp) target_link_libraries(bm_logger PRIVATE sfizz::sfizz) diff --git a/src/sfizz/SIMDHelpers.cpp b/src/sfizz/SIMDHelpers.cpp index c631a0ca..c86cecf8 100644 --- a/src/sfizz/SIMDHelpers.cpp +++ b/src/sfizz/SIMDHelpers.cpp @@ -41,6 +41,7 @@ struct SIMDDispatch { decltype(&meanScalar) mean = &meanScalar; decltype(&meanSquaredScalar) meanSquared = &meanSquaredScalar; decltype(&clampAllScalar) clampAll = &clampAllScalar; + decltype(&allWithinScalar) allWithin = &allWithinScalar; private: std::array(SIMDOps::_sentinel)> simdStatus; @@ -86,6 +87,7 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(mean) SIMD_OP(meanSquared) SIMD_OP(clampAll) + SIMD_OP(allWithin) } #undef SIMD_OP } @@ -122,6 +124,7 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(mean) SIMD_OP(meanSquared) SIMD_OP(clampAll) + SIMD_OP(allWithin) } } #undef SIMD_OP @@ -163,6 +166,7 @@ void SIMDDispatch::resetStatus() setStatus(SIMDOps::meanSquared, false); setStatus(SIMDOps::upsampling, true); setStatus(SIMDOps::clampAll, false); + setStatus(SIMDOps::allWithin, true); } /// @@ -308,7 +312,13 @@ void diff(const float* input, float* output, unsigned size) noexcept template <> void clampAll(float* input, float low, float high, unsigned size) noexcept { - return simdDispatch().clampAll(input, low, high, size); + simdDispatch().clampAll(input, low, high, size); +} + +template <> +bool allWithin(const float* input, float low, float high, unsigned size) noexcept +{ + return simdDispatch().allWithin(input, low, high, size); } } diff --git a/src/sfizz/SIMDHelpers.h b/src/sfizz/SIMDHelpers.h index 17b21c88..1cfa48d1 100644 --- a/src/sfizz/SIMDHelpers.h +++ b/src/sfizz/SIMDHelpers.h @@ -60,6 +60,7 @@ enum class SIMDOps { meanSquared, upsampling, clampAll, + allWithin, _sentinel // }; @@ -628,7 +629,6 @@ void diff(absl::Span input, absl::Span output) noexcept * * @tparam T the underlying type * @param input - * @param output * @param low * @param high * @param size @@ -648,4 +648,28 @@ void clampAll(absl::Span input, T low, T high) noexcept clampAll(input.data(), low, high, input.size()); } +/** + * @brief Check that all values are within bounds (inclusive) + * + * @tparam T the underlying type + * @param input + * @param low + * @param high + * @param size + */ +template +bool allWithin(const T* input, T low, T high, unsigned size) noexcept +{ + return allWithinScalar(input, low, high, size); +} + +template <> +bool allWithin(const float* input, float low, float high, unsigned size) noexcept; + +template +bool allWithin(absl::Span input, T low, T high) noexcept +{ + return allWithin(input.data(), low, high, input.size()); +} + } // namespace sfz diff --git a/src/sfizz/simd/HelpersSSE.cpp b/src/sfizz/simd/HelpersSSE.cpp index 7a0e7beb..3ae8b984 100644 --- a/src/sfizz/simd/HelpersSSE.cpp +++ b/src/sfizz/simd/HelpersSSE.cpp @@ -503,3 +503,44 @@ void clampAllSSE(float* input, float low, float high, unsigned size) noexcept incrementAll(input); } } + +bool allWithinSSE(const float* input, float low, float high, unsigned size) noexcept +{ + if (size == 0) + return true; + + if (low > high) + std::swap(low, high); + + const auto sentinel = input + size; + +#if SFIZZ_HAVE_SSE2 + const auto* lastAligned = prevAligned(sentinel); + while (unaligned(input) && input < lastAligned){ + if (*input < low || *input > high) + return false; + + incrementAll(input); + } + + const auto mmLow = _mm_set1_ps(low); + const auto mmHigh = _mm_set1_ps(high); + while (input < lastAligned) { + const auto mmIn = _mm_load_ps(input); + const auto mmOutside = _mm_or_ps(_mm_cmplt_ps(mmIn, mmLow), _mm_cmpgt_ps(mmIn, mmHigh)); + if (_mm_movemask_ps(mmOutside) != 0) + return false; + + incrementAll(input); + } +#endif + + while (input < sentinel) { + if (*input < low || *input > high) + return false; + + incrementAll(input); + } + + return true; +} diff --git a/src/sfizz/simd/HelpersSSE.h b/src/sfizz/simd/HelpersSSE.h index 7100ee89..cff28650 100644 --- a/src/sfizz/simd/HelpersSSE.h +++ b/src/sfizz/simd/HelpersSSE.h @@ -26,3 +26,4 @@ float meanSquaredSSE(const float* vector, unsigned size) noexcept; void cumsumSSE(const float* input, float* output, unsigned size) noexcept; void diffSSE(const float* input, float* output, unsigned size) noexcept; void clampAllSSE(float* input, float low, float high, unsigned size) noexcept; +bool allWithinSSE(const float* input, float low, float high, unsigned size) noexcept; diff --git a/src/sfizz/simd/HelpersScalar.h b/src/sfizz/simd/HelpersScalar.h index aa2017d1..d5ac1162 100644 --- a/src/sfizz/simd/HelpersScalar.h +++ b/src/sfizz/simd/HelpersScalar.h @@ -200,3 +200,23 @@ void clampAllScalar(T* input, T low, T high, unsigned size ) noexcept incrementAll(input); } } + +template +bool allWithinScalar(const T* input, T low, T high, unsigned size ) noexcept +{ + if (size == 0) + return true; + + if (low > high) + std::swap(low, high); + + const auto sentinel = input + size; + while (input < sentinel) { + if (*input < low || *input > high) + return false; + + incrementAll(input); + } + + return true; +} diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index ebc94feb..2dbfd94b 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -839,3 +839,14 @@ TEST_CASE("[Helpers] clampAll (SIMD vs scalar)") sfz::clampAll(absl::MakeSpan(inputSIMD), 10.0f, 50.0f); REQUIRE( approxEqual(inputScalar, inputSIMD) ); } + +TEST_CASE("[Helpers] allWithin") +{ + std::array input { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f }; + sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, false); + REQUIRE( sfz::allWithin(input, 0.5f, 11.0f) ); + REQUIRE( !sfz::allWithin(input, 2.5f, 8.0f) ); + sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, true); + REQUIRE( sfz::allWithin(input, 0.5f, 11.0f) ); + REQUIRE( !sfz::allWithin(input, 2.5f, 8.0f) ); +} From 9d8ebaf71840fa9b58d26359471e128c76f31d77 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 12 Aug 2020 19:23:50 +0200 Subject: [PATCH 104/445] Add VST info panel --- vst/SfizzVstController.cpp | 10 +++ vst/SfizzVstController.h | 4 + vst/SfizzVstEditor.cpp | 155 +++++++++++++++++++++++++++++++++++++ vst/SfizzVstEditor.h | 8 ++ vst/SfizzVstProcessor.cpp | 24 +++++- vst/SfizzVstProcessor.h | 4 + vst/SfizzVstState.h | 9 +++ 7 files changed, 213 insertions(+), 1 deletion(-) diff --git a/vst/SfizzVstController.cpp b/vst/SfizzVstController.cpp index b8c75a10..8f13bab2 100644 --- a/vst/SfizzVstController.cpp +++ b/vst/SfizzVstController.cpp @@ -283,6 +283,16 @@ tresult SfizzVstController::notify(Vst::IMessage* message) _state.scalaFile.assign(static_cast(data), size); } + else if (!strcmp(id, "NotifiedPlayState")) { + const void* data = nullptr; + uint32 size = 0; + result = attr->getBinary("PlayState", data, size); + + if (result != kResultTrue) + return result; + + _playState = *static_cast(data); + } for (StateListener* listener : _stateListeners) listener->onStateChanged(); diff --git a/vst/SfizzVstController.h b/vst/SfizzVstController.h index 854a3722..bd363a2c 100644 --- a/vst/SfizzVstController.h +++ b/vst/SfizzVstController.h @@ -56,6 +56,9 @@ public: const SfizzUiState& getSfizzUiState() const { return _uiState; } SfizzUiState& getSfizzUiState() { return _uiState; } + const SfizzPlayState& getSfizzPlayState() const { return _playState; } + SfizzPlayState& getSfizzPlayState() { return _playState; } + void addSfizzStateListener(StateListener* listener); void removeSfizzStateListener(StateListener* listener); @@ -67,5 +70,6 @@ public: private: SfizzVstState _state; SfizzUiState _uiState; + SfizzPlayState _playState {}; std::vector _stateListeners; }; diff --git a/vst/SfizzVstEditor.cpp b/vst/SfizzVstEditor.cpp index 6b35b213..5863cdc5 100644 --- a/vst/SfizzVstEditor.cpp +++ b/vst/SfizzVstEditor.cpp @@ -508,6 +508,139 @@ void SfizzVstEditor::createFrameContents() _subPanels[kPanelTuning] = panel; } + // info panel + { + panel = new CViewContainer(bounds); + frame->addView(panel); + panel->setTransparency(true); + + CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Information"); + topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); + topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + panel->addView(topLeftLabel); + + CRect row = topRow; + row.top += 45.0; + row.bottom += 45.0; + row.left += 20.0; + row.right -= 20.0; + + static const CCoord interRow = 20.0; + static const CCoord interColumn = 20.0; + static const int numColumns = 3; + + auto nthColumn = [&row](int colIndex) -> CRect { + CRect div = row; + CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; + div.left = div.left + colIndex * (columnWidth + interColumn); + div.right = div.left + columnWidth; + return div; + }; + + CTextLabel* label; + + label = new CTextLabel(nthColumn(0), "Curves"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + _infoCurvesLabel = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Masters"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + _infoMastersLabel = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Groups"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + _infoGroupsLabel = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Regions"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + _infoRegionsLabel = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Samples"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + _infoSamplesLabel = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Voices"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + _infoVoicesLabel = label; + panel->addView(label); + + _subPanels[kPanelInfo] = panel; + } + // all panels for (unsigned currentPanel = 0; currentPanel < kNumPanels; ++currentPanel) { panel = _subPanels[currentPanel]; @@ -528,6 +661,7 @@ void SfizzVstEditor::createFrameContents() case kPanelGeneral: text = "File"; break; case kPanelSettings: text = "Setup"; break; case kPanelTuning: text = "Tuning"; break; + case kPanelInfo: text = "Info"; break; default: text = "?"; break; } @@ -549,7 +683,9 @@ void SfizzVstEditor::updateStateDisplay() SfizzVstController* controller = getController(); const SfizzVstState& state = controller->getSfizzState(); const SfizzUiState& uiState = controller->getSfizzUiState(); + const SfizzPlayState& playState = controller->getSfizzPlayState(); + /// updateSfzFileLabel(state.sfzFile); if (_volumeSlider) _volumeSlider->setValue(state.volume); @@ -574,6 +710,25 @@ void SfizzVstEditor::updateStateDisplay() _stretchedTuningSlider->setValue(state.stretchedTuning); updateStretchedTuningLabel(state.stretchedTuning); + /// + struct InfoLabel { const uint32* src; CTextLabel* dst; }; + for (const auto& item : { + InfoLabel{&playState.curves, _infoCurvesLabel}, + InfoLabel{&playState.masters, _infoMastersLabel}, + InfoLabel{&playState.groups, _infoGroupsLabel}, + InfoLabel{&playState.regions, _infoRegionsLabel}, + InfoLabel{&playState.preloadedSamples, _infoSamplesLabel}, + InfoLabel{&playState.activeVoices, _infoVoicesLabel} }) + { + if (item.dst) { + char text[64]; + sprintf(text, "%u", *item.src); + text[sizeof(text) - 1] = '\0'; + item.dst->setText(text); + } + } + + /// setActivePanel(uiState.activePanel); } diff --git a/vst/SfizzVstEditor.h b/vst/SfizzVstEditor.h index f0228b7e..7b6e8392 100644 --- a/vst/SfizzVstEditor.h +++ b/vst/SfizzVstEditor.h @@ -73,6 +73,7 @@ private: // kPanelControls, kPanelSettings, kPanelTuning, + kPanelInfo, kNumPanels, }; @@ -111,6 +112,13 @@ private: CSliderBase *_stretchedTuningSlider = nullptr; CTextLabel* _stretchedTuningLabel = nullptr; + CTextLabel* _infoCurvesLabel = nullptr; + CTextLabel* _infoMastersLabel = nullptr; + CTextLabel* _infoGroupsLabel = nullptr; + CTextLabel* _infoRegionsLabel = nullptr; + CTextLabel* _infoSamplesLabel = nullptr; + CTextLabel* _infoVoicesLabel = nullptr; + #if !defined(__APPLE__) && !defined(_WIN32) SharedPointer _runLoop; #endif diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index e64e184f..bedde084 100644 --- a/vst/SfizzVstProcessor.cpp +++ b/vst/SfizzVstProcessor.cpp @@ -132,7 +132,8 @@ tresult PLUGIN_API SfizzVstProcessor::setActive(TBool state) synth->setSampleRate(processSetup.sampleRate); synth->setSamplesPerBlock(processSetup.maxSamplesPerBlock); - _fileChangePeriod = static_cast(processSetup.sampleRate); + _fileChangePeriod = static_cast(1.0 * processSetup.sampleRate); + _playStateChangePeriod = static_cast(50e-3 * processSetup.sampleRate); _workRunning = true; _worker = std::thread([this]() { doBackgroundWork(); }); @@ -203,6 +204,20 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) _semaToWorker.post(); } + _playStateChangeCounter += numFrames; + if (_playStateChangeCounter > _playStateChangePeriod) { + _playStateChangeCounter %= _playStateChangePeriod; + SfizzPlayState playState; + playState.curves = synth.getNumCurves(); + playState.masters = synth.getNumMasters(); + playState.groups = synth.getNumGroups(); + playState.regions = synth.getNumRegions(); + playState.preloadedSamples = synth.getNumPreloadedSamples(); + playState.activeVoices = synth.getNumActiveVoices(); + if (writeWorkerMessage("NotifyPlayState", &playState, sizeof(playState))) + _semaToWorker.post(); + } + return kResultTrue; } @@ -465,6 +480,13 @@ void SfizzVstProcessor::doBackgroundWork() _synth->loadScalaFile(_state.scalaFile); } } + else if (!std::strcmp(id, "NotifyPlayState")) { + SfizzPlayState playState = *msg->payload(); + Steinberg::OPtr notification { allocateMessage() }; + notification->setMessageID("NotifiedPlayState"); + notification->getAttributes()->setBinary("PlayState", &playState, sizeof(playState)); + sendMessage(notification); + } } } diff --git a/vst/SfizzVstProcessor.h b/vst/SfizzVstProcessor.h index 76af84a6..1eb68cea 100644 --- a/vst/SfizzVstProcessor.h +++ b/vst/SfizzVstProcessor.h @@ -64,6 +64,10 @@ private: uint32 _fileChangeCounter = 0; uint32 _fileChangePeriod = 0; + // state notification periodic timer + uint32 _playStateChangeCounter = 0; + uint32 _playStateChangePeriod = 0; + // time info int _timeSigNumerator = 0; int _timeSigDenominator = 0; diff --git a/vst/SfizzVstState.h b/vst/SfizzVstState.h index becafad3..f356d04e 100644 --- a/vst/SfizzVstState.h +++ b/vst/SfizzVstState.h @@ -62,6 +62,15 @@ public: tresult store(IBStream* state) const; }; +struct SfizzPlayState { + uint32 curves; + uint32 masters; + uint32 groups; + uint32 regions; + uint32 preloadedSamples; + uint32 activeVoices; +}; + struct SfizzParameterRange { float def = 0.0; float min = 0.0; From c629f6803b0a0aa31408858d83aa2e6417479e8f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 12 Aug 2020 19:40:33 +0200 Subject: [PATCH 105/445] Some more tests --- tests/SIMDHelpersT.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index 2dbfd94b..805bb0f9 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -846,7 +846,11 @@ TEST_CASE("[Helpers] allWithin") sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, false); REQUIRE( sfz::allWithin(input, 0.5f, 11.0f) ); REQUIRE( !sfz::allWithin(input, 2.5f, 8.0f) ); + REQUIRE( !sfz::allWithin(input, 0.0f, 5.0f) ); + REQUIRE( !sfz::allWithin(input, -1.0f, 7.0f) ); sfz::setSIMDOpStatus(sfz::SIMDOps::allWithin, true); REQUIRE( sfz::allWithin(input, 0.5f, 11.0f) ); REQUIRE( !sfz::allWithin(input, 2.5f, 8.0f) ); + REQUIRE( !sfz::allWithin(input, 0.0f, 5.0f) ); + REQUIRE( !sfz::allWithin(input, -1.0f, 7.0f) ); } From 3b1ce0e1641c5bb0db04790b68da9b0e3308c3cd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 12 Aug 2020 05:06:53 +0200 Subject: [PATCH 106/445] Implement the correct LFO fade in --- src/sfizz/LFO.cpp | 34 +++++++++++++++++++++++----------- src/sfizz/LFO.h | 5 +++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/sfizz/LFO.cpp b/src/sfizz/LFO.cpp index cb7f0747..b14dba95 100644 --- a/src/sfizz/LFO.cpp +++ b/src/sfizz/LFO.cpp @@ -23,8 +23,7 @@ struct LFO::Impl { // state size_t delayFramesLeft_ = 0; - float fadeInPole_ = 0; - float fadeInMemory_ = 0; + float fadePosition_ = 0; std::array subPhases_ {{}}; std::array sampleHoldMem_ {{}}; }; @@ -62,9 +61,7 @@ void LFO::start() const float delay = desc.delay; impl.delayFramesLeft_ = (delay > 0) ? static_cast(std::ceil(sampleRate * delay)) : 0u; - const float fade = desc.fade; - impl.fadeInPole_ = (fade > 0) ? std::exp(-1.0 / (fade * sampleRate)) : 0.0f; - impl.fadeInMemory_ = 0; + impl.fadePosition_ = (desc.fade > 0) ? 0.0f : 1.0f; } template <> @@ -283,13 +280,28 @@ void LFO::process(absl::Span out) } } - float fadeIn = impl.fadeInMemory_; - const float fadeInPole = impl.fadeInPole_; - for (size_t i = 0; i < numFrames; ++i) { - out[i] *= fadeIn; - fadeIn = fadeInPole * fadeIn + (1 - fadeInPole); + processFadeIn(out); +} + +void LFO::processFadeIn(absl::Span out) +{ + Impl& impl = *impl_; + const LFODescription& desc = *impl.desc_; + const float samplePeriod = 1.0f / impl.sampleRate_; + size_t numFrames = out.size(); + + float fadePosition = impl.fadePosition_; + if (fadePosition >= 1.0f) + return; + + const float fadeTime = desc.fade; + + for (size_t i = 0; i < numFrames && fadePosition < 1; ++i) { + out[i] *= fadePosition; + fadePosition = std::min(1.0f, fadePosition + samplePeriod / fadeTime); } - impl.fadeInMemory_ = fadeIn; + + impl.fadePosition_ = fadePosition; } } // namespace sfz diff --git a/src/sfizz/LFO.h b/src/sfizz/LFO.h index fbd9dd14..3d75cad3 100644 --- a/src/sfizz/LFO.h +++ b/src/sfizz/LFO.h @@ -104,6 +104,11 @@ private: */ void processSteps(absl::Span out); + /** + Process the fade in gain, and apply it to the buffer. + */ + void processFadeIn(absl::Span out); + private: struct Impl; std::unique_ptr impl_; From b71a2fb94b2d1af6f7a220b1bc6b26dff34e5cdd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 12 Aug 2020 10:59:19 +0200 Subject: [PATCH 107/445] Explicitely compute the step out of the loop --- src/sfizz/LFO.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sfizz/LFO.cpp b/src/sfizz/LFO.cpp index b14dba95..33d31f70 100644 --- a/src/sfizz/LFO.cpp +++ b/src/sfizz/LFO.cpp @@ -295,10 +295,11 @@ void LFO::processFadeIn(absl::Span out) return; const float fadeTime = desc.fade; + const float fadeStep = samplePeriod / fadeTime; for (size_t i = 0; i < numFrames && fadePosition < 1; ++i) { out[i] *= fadePosition; - fadePosition = std::min(1.0f, fadePosition + samplePeriod / fadeTime); + fadePosition = std::min(1.0f, fadePosition + fadeStep); } impl.fadePosition_ = fadePosition; From b46076ba3b02d5631eecb3bd1ed436f307ae99f2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 13 Aug 2020 03:34:03 +0200 Subject: [PATCH 108/445] Command-line arguments for sfizz_plot_lfo --- tests/PlotLFO.cpp | 54 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/tests/PlotLFO.cpp b/tests/PlotLFO.cpp index d993236a..d8c535cc 100644 --- a/tests/PlotLFO.cpp +++ b/tests/PlotLFO.cpp @@ -18,6 +18,7 @@ #include "sfizz/Synth.h" #include "sfizz/LFO.h" #include "sfizz/LFODescription.h" +#include "cxxopts.hpp" #include #include #include @@ -25,16 +26,8 @@ //============================================================================== -static constexpr double sampleRate = 44100.0; // sample rate used to compute -static constexpr double duration = 5.0; // length in seconds - -/** - Print usage information - */ -static void usage() -{ - std::cerr << "Usage: sfizz_plot_lfo " "\n"; -} +static double sampleRate = 1000.0; // sample rate used to compute +static double duration = 5.0; // length in seconds static std::vector lfoDescriptionFromSfzFile(const fs::path &sfzPath, bool &success) { @@ -61,16 +54,49 @@ static std::vector lfoDescriptionFromSfzFile(const fs::path */ int main(int argc, char* argv[]) { - if (argc < 2 || argc > 2) { - usage(); + cxxopts::Options options("sfizz_plot_lfo", "Compute LFO and generate plot data"); + + options.add_options() + ("s,samplerate", "Sample rate", cxxopts::value(sampleRate)) + ("d,duration", "Duration", cxxopts::value(duration)) + ("h,help", "Print usage") + ; + options.positional_help("sfz-file"); + + fs::path sfzPath; + + try { + cxxopts::ParseResult result = options.parse(argc, argv); + options.parse_positional({ "sfz-file" }); + + if (result.count("help")) { + std::cout << options.help() << std::endl; + return 0; + } + + if (argc != 2) { + std::cerr << "Please indicate the SFZ file to process.\n"; + return 1; + } + + sfzPath = argv[1]; + } + catch (cxxopts::OptionException& ex) { + std::cerr << ex.what() << "\n"; return 1; } - fs::path sfzPath = argv[1]; bool success = false; const std::vector desc = lfoDescriptionFromSfzFile(sfzPath, success); - if (!success) + if (!success){ + std::cerr << "Could not extract LFO descriptions from SFZ file.\n"; return 1; + } + + if (sampleRate <= 0) { + std::cerr << "The sample rate provided is invalid.\n"; + return 1; + } size_t numLfos = desc.size(); std::vector lfos(numLfos); From d47bca6f4ee4be4f1857dfac66435580d7ac6dce Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 13 Aug 2020 03:48:10 +0200 Subject: [PATCH 109/445] Allow the LFO tool to set options and output FLAC --- tests/PlotLFO.cpp | 80 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/tests/PlotLFO.cpp b/tests/PlotLFO.cpp index d8c535cc..06ccc553 100644 --- a/tests/PlotLFO.cpp +++ b/tests/PlotLFO.cpp @@ -18,16 +18,23 @@ #include "sfizz/Synth.h" #include "sfizz/LFO.h" #include "sfizz/LFODescription.h" +#include "sfizz/MathHelpers.h" #include "cxxopts.hpp" #include #include #include #include +#ifdef _WIN32 +#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 +#endif +#include //============================================================================== static double sampleRate = 1000.0; // sample rate used to compute static double duration = 5.0; // length in seconds +static std::string outputFilename; +static bool saveFlac = false; static std::vector lfoDescriptionFromSfzFile(const fs::path &sfzPath, bool &success) { @@ -59,6 +66,8 @@ int main(int argc, char* argv[]) options.add_options() ("s,samplerate", "Sample rate", cxxopts::value(sampleRate)) ("d,duration", "Duration", cxxopts::value(duration)) + ("o,output", "Output file", cxxopts::value(outputFilename)) + ("F,flac", "Save output as FLAC", cxxopts::value(saveFlac)) ("h,help", "Print usage") ; options.positional_help("sfz-file"); @@ -119,11 +128,72 @@ int main(int argc, char* argv[]) lfos[l].process(lfoOutputs[l]); } - for (size_t i = 0; i < numFrames; ++i) { - std::cout << (i / sampleRate); - for (size_t l = 0; l < numLfos; ++l) - std::cout << ' ' << lfoOutputs[l][i]; - std::cout << '\n'; + if (saveFlac) { + if (outputFilename.empty()) { + std::cerr << "Please indicate the audio file to save.\n"; + return 1; + } + + fs::path outputPath = fs::u8path(outputFilename); + SndfileHandle snd( +#ifndef _WIN32 + outputPath.c_str(), +#else + outputPath.wstring().c_str(), +#endif + SFM_WRITE, SF_FORMAT_FLAC|SF_FORMAT_PCM_16, numLfos, sampleRate); + + std::unique_ptr frame(new float[numLfos]); + size_t numClips = 0; + + for (size_t i = 0; i < numFrames; ++i) { + for (size_t l = 0; l < numLfos; ++l) { + float orig = lfoOutputs[l][i]; + float clamped = clamp(orig, -1.0f, 1.0f); + numClips += clamped != orig; + frame[l] = clamped; + } + snd.writef(frame.get(), 1); + } + snd.writeSync(); + + if (snd.error()) { + std::error_code ec; + fs::remove(outputPath, ec); + std::cerr << "Could not save audio to the output file.\n"; + return 1; + } + + if (numClips > 0) + std::cerr << "Warning: the audio output has been clipped on " << numClips << " frames.\n"; + } + else { + std::ostream* os = &std::cout; + fs::ofstream of; + + fs::path outputPath; + if (!outputFilename.empty()) { + outputPath = fs::u8path(outputFilename); + of.open(outputPath); + os = &of; + } + + for (size_t i = 0; i < numFrames; ++i) { + *os << (i / sampleRate); + for (size_t l = 0; l < numLfos; ++l) + *os << ' ' << lfoOutputs[l][i]; + *os << '\n'; + } + + if (os == &of) { + of.flush(); + if (!of) { + std::error_code ec; + fs::remove(outputPath, ec); + std::cerr << "Could not save data to the output file.\n"; + return 1; + } + } } return 0; From 73b1ed961de4ba53e51a470b08a8b8c8bb8e85f2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 12 Aug 2020 00:31:19 +0200 Subject: [PATCH 110/445] Add LFO regression tests The reference files have been visually validated Tweaks for the LFO regression test --- tests/CMakeLists.txt | 3 + tests/DataHelpers.cpp | 89 ++++ tests/DataHelpers.h | 29 ++ tests/LFOT.cpp | 117 +++++ tests/TestFiles/lfo_subwave.sfz | 19 - tests/lfo/compare_lfo.py | 27 ++ tests/lfo/lfo_fade_and_delay.sfz | 7 + tests/lfo/lfo_fade_and_delay_reference.dat | 500 +++++++++++++++++++++ tests/lfo/lfo_subwave.sfz | 31 ++ tests/lfo/lfo_subwave_reference.dat | 500 +++++++++++++++++++++ tests/lfo/lfo_waves.sfz | 23 + tests/lfo/lfo_waves_reference.dat | 500 +++++++++++++++++++++ tests/lfo/plot_lfo.py | 51 +++ 13 files changed, 1877 insertions(+), 19 deletions(-) create mode 100644 tests/DataHelpers.cpp create mode 100644 tests/DataHelpers.h create mode 100644 tests/LFOT.cpp delete mode 100644 tests/TestFiles/lfo_subwave.sfz create mode 100755 tests/lfo/compare_lfo.py create mode 100644 tests/lfo/lfo_fade_and_delay.sfz create mode 100644 tests/lfo/lfo_fade_and_delay_reference.dat create mode 100644 tests/lfo/lfo_subwave.sfz create mode 100644 tests/lfo/lfo_subwave_reference.dat create mode 100644 tests/lfo/lfo_waves.sfz create mode 100644 tests/lfo/lfo_waves_reference.dat create mode 100755 tests/lfo/plot_lfo.py diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9abe75de..b9926ab9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,9 @@ set(SFIZZ_TEST_SOURCES TuningT.cpp ConcurrencyT.cpp ModulationsT.cpp + LFOT.cpp + DataHelpers.h + DataHelpers.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) diff --git a/tests/DataHelpers.cpp b/tests/DataHelpers.cpp new file mode 100644 index 00000000..1f0bd949 --- /dev/null +++ b/tests/DataHelpers.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "DataHelpers.h" +#include +#include +#include +#include +#include + +void load_txt(DataPoints& dp, std::istream& in) +{ + struct RawValue { + bool rowJump; + float value; + }; + + std::vector raw; + raw.reserve(1024); + + // read raw value data + { + std::string line; + line.reserve(256); + + while (std::getline(in, line)) { + size_t commentPos = line.find('#'); + if (commentPos == line.npos) + line = line.substr(0, commentPos); + + std::istringstream lineIn(line); + + RawValue rv; + rv.rowJump = true; + while (lineIn >> rv.value) { + raw.push_back(rv); + rv.rowJump = false; + } + } + } + + if (raw.empty()) { + dp.rows = 0; + dp.cols = 0; + dp.data.reset(); + return; + } + + // count rows and columns + size_t numRows = 0; + size_t numCols = 0; + { + size_t c = 0; + for (const RawValue& rv : raw) { + if (!rv.rowJump) + ++c; + else { + numRows += c != 0; + c = 1; + } + numCols = std::max(numCols, c); + } + numRows += c != 0; + } + + // fill the data + float* data = new float[numRows * numCols]; + dp.rows = numRows; + dp.cols = numCols; + dp.data.reset(data); + for (size_t i = 0, j = 0; i < numRows * numCols; ) { + size_t c = 1; + data[i++] = raw[j++].value; + for (; j < raw.size() && !raw[j].rowJump; ++c) + data[i++] = raw[j++].value; + for ( ; c < numCols; ++c) + data[i++] = 0.0f; + } +} + +bool load_txt_file(DataPoints& dp, const fs::path& path) +{ + fs::ifstream in(path); + load_txt(dp, in); + return !in.bad(); +} diff --git a/tests/DataHelpers.h b/tests/DataHelpers.h new file mode 100644 index 00000000..7f337a49 --- /dev/null +++ b/tests/DataHelpers.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include +#include +#include + +struct DataPoints { + size_t rows = 0; + size_t cols = 0; + std::unique_ptr data; + + const float& operator()(size_t r, size_t c) const noexcept + { + return data[r * cols + c]; + } + float& operator()(size_t r, size_t c) noexcept + { + return data[r * cols + c]; + } +}; + +void load_txt(DataPoints& dp, std::istream& in); +bool load_txt_file(DataPoints& dp, const fs::path& path); diff --git a/tests/LFOT.cpp b/tests/LFOT.cpp new file mode 100644 index 00000000..84c2795d --- /dev/null +++ b/tests/LFOT.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "DataHelpers.h" +#include "sfizz/Synth.h" +#include "sfizz/LFO.h" +#include "catch2/catch.hpp" + +static bool computeLFO(DataPoints& dp, const fs::path& sfzPath, double sampleRate, size_t numFrames) +{ + sfz::Synth synth; + + if (!synth.loadSfzFile(sfzPath)) + return false; + + if (synth.getNumRegions() != 1) + return false; + + const std::vector& desc = synth.getRegionView(0)->lfos; + size_t numLfos = desc.size(); + std::vector lfos(numLfos); + + for (size_t l = 0; l < numLfos; ++l) { + lfos[l].setSampleRate(sampleRate); + lfos[l].configure(&desc[l]); + } + + std::vector outputMemory(numLfos * numFrames); + + for (size_t l = 0; l < numLfos; ++l) { + lfos[l].start(); + } + + std::vector> lfoOutputs(numLfos); + for (size_t l = 0; l < numLfos; ++l) { + lfoOutputs[l] = absl::MakeSpan(&outputMemory[l * numFrames], numFrames); + lfos[l].process(lfoOutputs[l]); + } + + dp.rows = numFrames; + dp.cols = numLfos + 1; + dp.data.reset(new float[dp.rows * dp.cols]); + + for (size_t i = 0; i < numFrames; ++i) { + dp(i, 0) = i / sampleRate; + for (size_t l = 0; l < numLfos; ++l) + dp(i, 1 + l) = lfoOutputs[l][i]; + } + + return true; +} + +double meanSquareError(const float* a, const float* b, size_t count, size_t step) +{ + double sum = 0; + for (size_t i = 0; i < count; ++i) { + double diff = a[i * step] - b[i * step]; + sum += diff * diff; + } + return sum / count; +} + +static constexpr double mseThreshold = 1e-3; + +TEST_CASE("[LFO] Waves") +{ + DataPoints ref; + REQUIRE(load_txt_file(ref, "tests/lfo/lfo_waves_reference.dat")); + + DataPoints cur; + REQUIRE(computeLFO(cur, "tests/lfo/lfo_waves.sfz", 100.0, ref.rows)); + + REQUIRE(ref.rows == cur.rows); + REQUIRE(ref.cols == cur.cols); + + for (size_t l = 1; l < cur.cols; ++l) { + double mse = meanSquareError(&ref.data[l], &cur.data[l], ref.rows, ref.cols); + REQUIRE(mse < mseThreshold); + } +} + +TEST_CASE("[LFO] Subwave") +{ + DataPoints ref; + REQUIRE(load_txt_file(ref, "tests/lfo/lfo_subwave_reference.dat")); + + DataPoints cur; + REQUIRE(computeLFO(cur, "tests/lfo/lfo_subwave.sfz", 100.0, ref.rows)); + + REQUIRE(ref.rows == cur.rows); + REQUIRE(ref.cols == cur.cols); + + for (size_t l = 1; l < cur.cols; ++l) { + double mse = meanSquareError(&ref.data[l], &cur.data[l], ref.rows, ref.cols); + REQUIRE(mse < mseThreshold); + } +} + +TEST_CASE("[LFO] Fade and delay") +{ + DataPoints ref; + REQUIRE(load_txt_file(ref, "tests/lfo/lfo_fade_and_delay_reference.dat")); + + DataPoints cur; + REQUIRE(computeLFO(cur, "tests/lfo/lfo_fade_and_delay.sfz", 100.0, ref.rows)); + + REQUIRE(ref.rows == cur.rows); + REQUIRE(ref.cols == cur.cols); + + for (size_t l = 1; l < cur.cols; ++l) { + double mse = meanSquareError(&ref.data[l], &cur.data[l], ref.rows, ref.cols); + REQUIRE(mse < mseThreshold); + } +} diff --git a/tests/TestFiles/lfo_subwave.sfz b/tests/TestFiles/lfo_subwave.sfz deleted file mode 100644 index 80867c3d..00000000 --- a/tests/TestFiles/lfo_subwave.sfz +++ /dev/null @@ -1,19 +0,0 @@ - -sample=*noise -lokey=0 -hikey=127 -cutoff=1000.0 -fil_type=brf_2p -lfo1_cutoff=1200.0 -// -lfo1_freq=1 -lfo1_phase=0.5 -lfo1_wave=3 -lfo1_delay=0.5 -lfo1_fade=0.5 -lfo1_wave2=1 -lfo1_offset2=0.2 -lfo1_ratio2=0.7 -lfo1_scale2=0.3 -// -lfo2_freq=0.5 diff --git a/tests/lfo/compare_lfo.py b/tests/lfo/compare_lfo.py new file mode 100755 index 00000000..31fd7e12 --- /dev/null +++ b/tests/lfo/compare_lfo.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +import numpy as np +from argparse import ArgumentParser +import os + +parser = ArgumentParser(usage="Compare 2 files as outputted by sfizz_plot_lfo") +parser.add_argument("file", help="The file to test") +parser.add_argument("reference", help="The reference file") +parser.add_argument("--threshold", type=float, default=0.001, help="Mean squared error threshold") +args = parser.parse_args() + +assert os.path.exists(args.file), "The file to test does not exist" +assert os.path.exists(args.reference), "The reference file does not exist" + +reference_data = np.loadtxt(args.reference) +data = np.loadtxt(args.file) + +assert reference_data.shape == data.shape, "The shapes of the data and reference are different" + +mean_squared_error = np.mean((data - reference_data) ** 2) +print("MSE difference:", mean_squared_error) + +if (mean_squared_error > args.threshold): + exit(-1) + diff --git a/tests/lfo/lfo_fade_and_delay.sfz b/tests/lfo/lfo_fade_and_delay.sfz new file mode 100644 index 00000000..122e7e94 --- /dev/null +++ b/tests/lfo/lfo_fade_and_delay.sfz @@ -0,0 +1,7 @@ + +sample=*sine +// +lfo1_freq=1 +lfo1_wave=3 +lfo1_delay=0.5 +lfo1_fade=1 diff --git a/tests/lfo/lfo_fade_and_delay_reference.dat b/tests/lfo/lfo_fade_and_delay_reference.dat new file mode 100644 index 00000000..fa6a155f --- /dev/null +++ b/tests/lfo/lfo_fade_and_delay_reference.dat @@ -0,0 +1,500 @@ +0 0 +0.01 0 +0.02 0 +0.03 0 +0.04 0 +0.05 0 +0.06 0 +0.07 0 +0.08 0 +0.09 0 +0.1 0 +0.11 0 +0.12 0 +0.13 0 +0.14 0 +0.15 0 +0.16 0 +0.17 0 +0.18 0 +0.19 0 +0.2 0 +0.21 0 +0.22 0 +0.23 0 +0.24 0 +0.25 0 +0.26 0 +0.27 0 +0.28 0 +0.29 0 +0.3 0 +0.31 0 +0.32 0 +0.33 0 +0.34 0 +0.35 0 +0.36 0 +0.37 0 +0.38 0 +0.39 0 +0.4 0 +0.41 0 +0.42 0 +0.43 0 +0.44 0 +0.45 0 +0.46 0 +0.47 0 +0.48 0 +0.49 0 +0.5 0 +0.51 0.01 +0.52 0.02 +0.53 0.03 +0.54 0.04 +0.55 0.05 +0.56 0.06 +0.57 0.07 +0.58 0.08 +0.59 0.09 +0.6 0.1 +0.61 0.11 +0.62 0.12 +0.63 0.13 +0.64 0.14 +0.65 0.15 +0.66 0.16 +0.67 0.17 +0.68 0.18 +0.69 0.19 +0.7 0.2 +0.71 0.21 +0.72 0.22 +0.73 0.23 +0.74 0.24 +0.75 0.25 +0.76 0.26 +0.77 0.27 +0.78 0.28 +0.79 0.29 +0.8 0.3 +0.81 0.31 +0.82 0.32 +0.83 0.33 +0.84 0.34 +0.85 0.35 +0.86 0.36 +0.87 0.37 +0.88 0.38 +0.89 0.39 +0.9 0.4 +0.91 0.41 +0.92 0.42 +0.93 0.43 +0.94 0.44 +0.95 0.45 +0.96 0.46 +0.97 0.47 +0.98 0.48 +0.99 0.49 +1 0.5 +1.01 -0.51 +1.02 -0.52 +1.03 -0.53 +1.04 -0.54 +1.05 -0.55 +1.06 -0.56 +1.07 -0.57 +1.08 -0.58 +1.09 -0.59 +1.1 -0.6 +1.11 -0.61 +1.12 -0.62 +1.13 -0.63 +1.14 -0.64 +1.15 -0.65 +1.16 -0.66 +1.17 -0.67 +1.18 -0.68 +1.19 -0.69 +1.2 -0.7 +1.21 -0.71 +1.22 -0.72 +1.23 -0.73 +1.24 -0.74 +1.25 -0.75 +1.26 -0.76 +1.27 -0.77 +1.28 -0.78 +1.29 -0.79 +1.3 -0.8 +1.31 -0.81 +1.32 -0.82 +1.33 -0.83 +1.34 -0.839999 +1.35 -0.849999 +1.36 -0.859999 +1.37 -0.869999 +1.38 -0.879999 +1.39 -0.889999 +1.4 -0.899999 +1.41 -0.909999 +1.42 -0.919999 +1.43 -0.929999 +1.44 -0.939999 +1.45 -0.949999 +1.46 -0.959999 +1.47 -0.969999 +1.48 -0.979999 +1.49 -0.989999 +1.5 -0.999999 +1.51 1 +1.52 1 +1.53 1 +1.54 1 +1.55 1 +1.56 1 +1.57 1 +1.58 1 +1.59 1 +1.6 1 +1.61 1 +1.62 1 +1.63 1 +1.64 1 +1.65 1 +1.66 1 +1.67 1 +1.68 1 +1.69 1 +1.7 1 +1.71 1 +1.72 1 +1.73 1 +1.74 1 +1.75 1 +1.76 1 +1.77 1 +1.78 1 +1.79 1 +1.8 1 +1.81 1 +1.82 1 +1.83 1 +1.84 1 +1.85 1 +1.86 1 +1.87 1 +1.88 1 +1.89 1 +1.9 1 +1.91 1 +1.92 1 +1.93 1 +1.94 1 +1.95 1 +1.96 1 +1.97 1 +1.98 1 +1.99 1 +2 1 +2.01 -1 +2.02 -1 +2.03 -1 +2.04 -1 +2.05 -1 +2.06 -1 +2.07 -1 +2.08 -1 +2.09 -1 +2.1 -1 +2.11 -1 +2.12 -1 +2.13 -1 +2.14 -1 +2.15 -1 +2.16 -1 +2.17 -1 +2.18 -1 +2.19 -1 +2.2 -1 +2.21 -1 +2.22 -1 +2.23 -1 +2.24 -1 +2.25 -1 +2.26 -1 +2.27 -1 +2.28 -1 +2.29 -1 +2.3 -1 +2.31 -1 +2.32 -1 +2.33 -1 +2.34 -1 +2.35 -1 +2.36 -1 +2.37 -1 +2.38 -1 +2.39 -1 +2.4 -1 +2.41 -1 +2.42 -1 +2.43 -1 +2.44 -1 +2.45 -1 +2.46 -1 +2.47 -1 +2.48 -1 +2.49 -1 +2.5 -1 +2.51 1 +2.52 1 +2.53 1 +2.54 1 +2.55 1 +2.56 1 +2.57 1 +2.58 1 +2.59 1 +2.6 1 +2.61 1 +2.62 1 +2.63 1 +2.64 1 +2.65 1 +2.66 1 +2.67 1 +2.68 1 +2.69 1 +2.7 1 +2.71 1 +2.72 1 +2.73 1 +2.74 1 +2.75 1 +2.76 1 +2.77 1 +2.78 1 +2.79 1 +2.8 1 +2.81 1 +2.82 1 +2.83 1 +2.84 1 +2.85 1 +2.86 1 +2.87 1 +2.88 1 +2.89 1 +2.9 1 +2.91 1 +2.92 1 +2.93 1 +2.94 1 +2.95 1 +2.96 1 +2.97 1 +2.98 1 +2.99 1 +3 1 +3.01 -1 +3.02 -1 +3.03 -1 +3.04 -1 +3.05 -1 +3.06 -1 +3.07 -1 +3.08 -1 +3.09 -1 +3.1 -1 +3.11 -1 +3.12 -1 +3.13 -1 +3.14 -1 +3.15 -1 +3.16 -1 +3.17 -1 +3.18 -1 +3.19 -1 +3.2 -1 +3.21 -1 +3.22 -1 +3.23 -1 +3.24 -1 +3.25 -1 +3.26 -1 +3.27 -1 +3.28 -1 +3.29 -1 +3.3 -1 +3.31 -1 +3.32 -1 +3.33 -1 +3.34 -1 +3.35 -1 +3.36 -1 +3.37 -1 +3.38 -1 +3.39 -1 +3.4 -1 +3.41 -1 +3.42 -1 +3.43 -1 +3.44 -1 +3.45 -1 +3.46 -1 +3.47 -1 +3.48 -1 +3.49 -1 +3.5 -1 +3.51 1 +3.52 1 +3.53 1 +3.54 1 +3.55 1 +3.56 1 +3.57 1 +3.58 1 +3.59 1 +3.6 1 +3.61 1 +3.62 1 +3.63 1 +3.64 1 +3.65 1 +3.66 1 +3.67 1 +3.68 1 +3.69 1 +3.7 1 +3.71 1 +3.72 1 +3.73 1 +3.74 1 +3.75 1 +3.76 1 +3.77 1 +3.78 1 +3.79 1 +3.8 1 +3.81 1 +3.82 1 +3.83 1 +3.84 1 +3.85 1 +3.86 1 +3.87 1 +3.88 1 +3.89 1 +3.9 1 +3.91 1 +3.92 1 +3.93 1 +3.94 1 +3.95 1 +3.96 1 +3.97 1 +3.98 1 +3.99 1 +4 1 +4.01 -1 +4.02 -1 +4.03 -1 +4.04 -1 +4.05 -1 +4.06 -1 +4.07 -1 +4.08 -1 +4.09 -1 +4.1 -1 +4.11 -1 +4.12 -1 +4.13 -1 +4.14 -1 +4.15 -1 +4.16 -1 +4.17 -1 +4.18 -1 +4.19 -1 +4.2 -1 +4.21 -1 +4.22 -1 +4.23 -1 +4.24 -1 +4.25 -1 +4.26 -1 +4.27 -1 +4.28 -1 +4.29 -1 +4.3 -1 +4.31 -1 +4.32 -1 +4.33 -1 +4.34 -1 +4.35 -1 +4.36 -1 +4.37 -1 +4.38 -1 +4.39 -1 +4.4 -1 +4.41 -1 +4.42 -1 +4.43 -1 +4.44 -1 +4.45 -1 +4.46 -1 +4.47 -1 +4.48 -1 +4.49 -1 +4.5 -1 +4.51 1 +4.52 1 +4.53 1 +4.54 1 +4.55 1 +4.56 1 +4.57 1 +4.58 1 +4.59 1 +4.6 1 +4.61 1 +4.62 1 +4.63 1 +4.64 1 +4.65 1 +4.66 1 +4.67 1 +4.68 1 +4.69 1 +4.7 1 +4.71 1 +4.72 1 +4.73 1 +4.74 1 +4.75 1 +4.76 1 +4.77 1 +4.78 1 +4.79 1 +4.8 1 +4.81 1 +4.82 1 +4.83 1 +4.84 1 +4.85 1 +4.86 1 +4.87 1 +4.88 1 +4.89 1 +4.9 1 +4.91 1 +4.92 1 +4.93 1 +4.94 1 +4.95 1 +4.96 1 +4.97 1 +4.98 1 +4.99 1 diff --git a/tests/lfo/lfo_subwave.sfz b/tests/lfo/lfo_subwave.sfz new file mode 100644 index 00000000..eb493764 --- /dev/null +++ b/tests/lfo/lfo_subwave.sfz @@ -0,0 +1,31 @@ + +sample=*noise +lokey=0 +hikey=127 +cutoff=1000.0 +fil_type=brf_2p +lfo1_cutoff=1200.0 +// +lfo1_freq=1 +lfo1_phase=180 +lfo1_wave=3 +// +lfo2_freq=1 +lfo2_phase=180 +lfo2_wave=3 +lfo2_wave2=1 +// +lfo3_freq=1 +lfo3_phase=180 +lfo3_wave=3 +lfo3_wave2=1 +lfo3_ratio2=2 + +// +lfo4_freq=1 +lfo4_phase=180 +lfo4_wave=3 +lfo4_wave2=1 +lfo4_ratio2=2 +lfo4_offset2=0.5 +lfo4_scale2=0.5 diff --git a/tests/lfo/lfo_subwave_reference.dat b/tests/lfo/lfo_subwave_reference.dat new file mode 100644 index 00000000..c4dd0ef1 --- /dev/null +++ b/tests/lfo/lfo_subwave_reference.dat @@ -0,0 +1,500 @@ +0 -1 -1 -1 -0.5 +0.01 -1 -0.9216 -0.8464 -0.4232 +0.02 -1 -0.8464 -0.7056 -0.3528 +0.03 -1 -0.7744 -0.5776 -0.2888 +0.04 -1 -0.7056 -0.4624 -0.2312 +0.05 -1 -0.64 -0.36 -0.18 +0.06 -1 -0.5776 -0.2704 -0.1352 +0.07 -1 -0.5184 -0.1936 -0.0968002 +0.08 -1 -0.4624 -0.1296 -0.0648002 +0.09 -1 -0.4096 -0.0784004 -0.0392002 +0.1 -1 -0.36 -0.0400003 -0.0200002 +0.11 -1 -0.3136 -0.0144002 -0.00720009 +0.12 -1 -0.2704 -0.00160009 -0.000800043 +0.13 -1 -0.230401 -0.00159991 -0.000799954 +0.14 -1 -0.1936 -0.0143998 -0.00719988 +0.15 -1 -0.16 -0.0399995 -0.0199998 +0.16 -1 -0.1296 -0.0783993 -0.0391997 +0.17 -1 -0.1024 -0.129599 -0.0647995 +0.18 -1 -0.0784004 -0.193599 -0.0967994 +0.19 -1 -0.0576003 -0.270398 -0.135199 +0.2 -1 -0.0400003 -0.359998 -0.179999 +0.21 -1 -0.0256003 -0.462398 -0.231199 +0.22 -1 -0.0144002 -0.577597 -0.288799 +0.23 -1 -0.00640017 -0.705597 -0.352799 +0.24 -1 -0.00160009 -0.846397 -0.423198 +0.25 -1 0 -0.999996 -0.499998 +0.26 -1 -0.00159991 -1.1536 -0.576798 +0.27 -1 -0.00639981 -1.2944 -0.647198 +0.28 -1 -0.0143998 -1.4224 -0.711198 +0.29 -1 -0.0255997 -1.5376 -0.768799 +0.3 -1 -0.0399995 -1.64 -0.819999 +0.31 -1 -0.0575994 -1.7296 -0.864799 +0.32 -1 -0.0783993 -1.8064 -0.903199 +0.33 -1 -0.102399 -1.8704 -0.935199 +0.34 -1 -0.129599 -1.9216 -0.960799 +0.35 -1 -0.159999 -1.96 -0.98 +0.36 -1 -0.193599 -1.9856 -0.9928 +0.37 -1 -0.230399 -1.9984 -0.9992 +0.38 -1 -0.270398 -1.9984 -0.9992 +0.39 -1 -0.313598 -1.9856 -0.9928 +0.4 -1 -0.359998 -1.96 -0.98 +0.41 -1 -0.409598 -1.9216 -0.960801 +0.42 -1 -0.462398 -1.8704 -0.935201 +0.43 -1 -0.518398 -1.8064 -0.903201 +0.44 -1 -0.577597 -1.7296 -0.864801 +0.45 -1 -0.639997 -1.64 -0.820001 +0.46 -1 -0.705597 -1.5376 -0.768801 +0.47 -1 -0.774397 -1.4224 -0.711201 +0.48 -1 -0.846397 -1.2944 -0.647201 +0.49 -1 -0.921596 -1.1536 -0.576801 +0.5 -1 -0.999996 -1 -0.500002 +0.51 1 0.921604 1.1536 1.5768 +0.52 1 0.846404 1.2944 1.6472 +0.53 1 0.774403 1.4224 1.7112 +0.54 1 0.705603 1.5376 1.7688 +0.55 1 0.640003 1.64 1.82 +0.56 1 0.577603 1.7296 1.8648 +0.57 1 0.518403 1.8064 1.9032 +0.58 1 0.462403 1.8704 1.9352 +0.59 1 0.409603 1.9216 1.9608 +0.6 1 0.360002 1.96 1.98 +0.61 1 0.313602 1.9856 1.9928 +0.62 1 0.270402 1.9984 1.9992 +0.63 1 0.230402 1.9984 1.9992 +0.64 1 0.193602 1.9856 1.9928 +0.65 1 0.160002 1.96 1.98 +0.66 1 0.129601 1.9216 1.9608 +0.67 1 0.102401 1.8704 1.9352 +0.68 1 0.078401 1.8064 1.9032 +0.69 1 0.0576009 1.7296 1.8648 +0.7 1 0.0400007 1.64 1.82 +0.71 1 0.0256006 1.5376 1.7688 +0.72 1 0.0144004 1.4224 1.7112 +0.73 1 0.00640029 1.29441 1.6472 +0.74 1 0.00160015 1.15361 1.5768 +0.75 1 0 1.00001 1.5 +0.76 1 0.00159985 0.846406 1.4232 +0.77 1 0.00639969 0.705606 1.3528 +0.78 1 0.0143996 0.577605 1.2888 +0.79 1 0.0255994 0.462405 1.2312 +0.8 1 0.0399992 0.360004 1.18 +0.81 1 0.0575991 0.270404 1.1352 +0.82 1 0.0783989 0.193603 1.0968 +0.83 1 0.102399 0.129602 1.0648 +0.84 1 0.129599 0.078402 1.0392 +0.85 1 0.159998 0.0400014 1.02 +0.86 1 0.193598 0.0144008 1.0072 +0.87 1 0.230398 0.00160027 1.0008 +0.88 1 0.270398 0.00159973 1.0008 +0.89 1 0.313598 0.0143992 1.0072 +0.9 1 0.359997 0.0399987 1.02 +0.91 1 0.409597 0.0783981 1.0392 +0.92 1 0.462397 0.129598 1.0648 +0.93 1 0.518397 0.193597 1.0968 +0.94 1 0.577596 0.270397 1.1352 +0.95 1 0.639996 0.359996 1.18 +0.96 1 0.705596 0.462396 1.2312 +0.97 1 0.774396 0.577595 1.2888 +0.98 1 0.846395 0.705595 1.3528 +0.99 1 0.921595 0.846394 1.4232 +1 1 0.999995 0.999994 1.5 +1.01 -1 -0.921605 -0.846405 -0.423203 +1.02 -1 -0.846405 -0.705605 -0.352803 +1.03 -1 -0.774405 -0.577605 -0.288802 +1.04 -1 -0.705605 -0.462404 -0.231202 +1.05 -1 -0.640005 -0.360004 -0.180002 +1.06 -1 -0.577604 -0.270403 -0.135202 +1.07 -1 -0.518404 -0.193603 -0.0968015 +1.08 -1 -0.462404 -0.129602 -0.0648012 +1.09 -1 -0.409604 -0.078402 -0.039201 +1.1 -1 -0.360004 -0.0400015 -0.0200007 +1.11 -1 -0.313603 -0.0144009 -0.00720045 +1.12 -1 -0.270403 -0.00160033 -0.000800163 +1.13 -1 -0.230403 -0.00159967 -0.000799835 +1.14 -1 -0.193603 -0.0143991 -0.00719953 +1.15 -1 -0.160003 -0.0399984 -0.0199992 +1.16 -1 -0.129602 -0.0783977 -0.0391988 +1.17 -1 -0.102402 -0.129597 -0.0647985 +1.18 -1 -0.0784019 -0.193596 -0.0967982 +1.19 -1 -0.0576016 -0.270396 -0.135198 +1.2 -1 -0.0400013 -0.359995 -0.179997 +1.21 -1 -0.0256011 -0.462394 -0.231197 +1.22 -1 -0.0144008 -0.577593 -0.288797 +1.23 -1 -0.00640059 -0.705592 -0.352796 +1.24 -1 -0.00160027 -0.846391 -0.423196 +1.25 -1 0 -0.99999 -0.499995 +1.26 -1 -0.00159973 -1.15359 -0.576796 +1.27 -1 -0.00639939 -1.29439 -0.647196 +1.28 -1 -0.0143991 -1.42239 -0.711196 +1.29 -1 -0.0255988 -1.53759 -0.768797 +1.3 -1 -0.0399985 -1.63999 -0.819997 +1.31 -1 -0.0575982 -1.72959 -0.864797 +1.32 -1 -0.0783979 -1.8064 -0.903198 +1.33 -1 -0.102398 -1.8704 -0.935198 +1.34 -1 -0.129597 -1.9216 -0.960799 +1.35 -1 -0.159997 -1.96 -0.979999 +1.36 -1 -0.193596 -1.9856 -0.992799 +1.37 -1 -0.230396 -1.9984 -0.9992 +1.38 -1 -0.270396 -1.9984 -0.9992 +1.39 -1 -0.313595 -1.9856 -0.992801 +1.4 -1 -0.359995 -1.96 -0.980001 +1.41 -1 -0.409595 -1.9216 -0.960801 +1.42 -1 -0.462394 -1.8704 -0.935202 +1.43 -1 -0.518394 -1.8064 -0.903202 +1.44 -1 -0.577593 -1.7296 -0.864802 +1.45 -1 -0.639993 -1.64001 -0.820003 +1.46 -1 -0.705593 -1.53761 -0.768803 +1.47 -1 -0.774392 -1.42241 -0.711203 +1.48 -1 -0.846392 -1.29441 -0.647204 +1.49 -1 -0.921591 -1.15361 -0.576804 +1.5 -1 -0.999991 -1.00001 -0.500004 +1.51 1 0.921608 1.15359 1.5768 +1.52 1 0.846408 1.29439 1.6472 +1.53 1 0.774408 1.42239 1.7112 +1.54 1 0.705607 1.53759 1.7688 +1.55 1 0.640007 1.63999 1.82 +1.56 1 0.577606 1.7296 1.8648 +1.57 1 0.518406 1.8064 1.9032 +1.58 1 0.462406 1.8704 1.9352 +1.59 1 0.409606 1.9216 1.9608 +1.6 1 0.360005 1.96 1.98 +1.61 1 0.313605 1.9856 1.9928 +1.62 1 0.270405 1.9984 1.9992 +1.63 1 0.230404 1.9984 1.9992 +1.64 1 0.193604 1.9856 1.9928 +1.65 1 0.160003 1.96 1.98 +1.66 1 0.129603 1.9216 1.9608 +1.67 1 0.102403 1.8704 1.9352 +1.68 1 0.0784024 1.80641 1.9032 +1.69 1 0.057602 1.72961 1.8648 +1.7 1 0.0400017 1.64001 1.82 +1.71 1 0.0256013 1.53761 1.7688 +1.72 1 0.014401 1.42241 1.7112 +1.73 1 0.00640064 1.29441 1.64721 +1.74 1 0.00160033 1.15361 1.57681 +1.75 1 0 1.00001 1.50001 +1.76 1 0.00159967 0.846412 1.42321 +1.77 1 0.00639933 0.705611 1.35281 +1.78 1 0.014399 0.57761 1.2888 +1.79 1 0.0255986 0.462409 1.2312 +1.8 1 0.0399983 0.360008 1.18 +1.81 1 0.0575979 0.270407 1.1352 +1.82 1 0.0783976 0.193605 1.0968 +1.83 1 0.102397 0.129605 1.0648 +1.84 1 0.129597 0.0784036 1.0392 +1.85 1 0.159996 0.0400025 1.02 +1.86 1 0.193596 0.0144015 1.0072 +1.87 1 0.230396 0.0016005 1.0008 +1.88 1 0.270395 0.00159949 1.0008 +1.89 1 0.313595 0.0143985 1.0072 +1.9 1 0.359994 0.0399975 1.02 +1.91 1 0.409594 0.0783965 1.0392 +1.92 1 0.462394 0.129596 1.0648 +1.93 1 0.518393 0.193595 1.0968 +1.94 1 0.577593 0.270394 1.1352 +1.95 1 0.639992 0.359993 1.18 +1.96 1 0.705592 0.462392 1.2312 +1.97 1 0.774391 0.577591 1.2888 +1.98 1 0.846391 0.70559 1.3528 +1.99 1 0.92159 0.846389 1.42319 +2 1 0.99999 0.999988 1.49999 +2.01 -1 -0.92161 -0.846411 -0.423205 +2.02 -1 -0.846409 -0.70561 -0.352805 +2.03 -1 -0.774409 -0.577609 -0.288805 +2.04 -1 -0.705609 -0.462408 -0.231204 +2.05 -1 -0.640008 -0.360007 -0.180004 +2.06 -1 -0.577608 -0.270406 -0.135203 +2.07 -1 -0.518408 -0.193605 -0.0968027 +2.08 -1 -0.462407 -0.129605 -0.0648023 +2.09 -1 -0.409607 -0.0784036 -0.0392018 +2.1 -1 -0.360006 -0.0400026 -0.0200013 +2.11 -1 -0.313606 -0.0144016 -0.00720078 +2.12 -1 -0.270406 -0.0016005 -0.000800252 +2.13 -1 -0.230405 -0.00159949 -0.000799745 +2.14 -1 -0.193605 -0.0143984 -0.0071992 +2.15 -1 -0.160004 -0.0399973 -0.0199986 +2.16 -1 -0.129604 -0.0783961 -0.0391981 +2.17 -1 -0.102404 -0.129595 -0.0647975 +2.18 -1 -0.0784032 -0.193594 -0.0967969 +2.19 -1 -0.0576028 -0.270393 -0.135196 +2.2 -1 -0.0400023 -0.359991 -0.179996 +2.21 -1 -0.0256019 -0.46239 -0.231195 +2.22 -1 -0.0144014 -0.577589 -0.288794 +2.23 -1 -0.00640094 -0.705587 -0.352794 +2.24 -1 -0.00160044 -0.846386 -0.423193 +2.25 -1 0 -0.999985 -0.499992 +2.26 -1 -0.00159949 -1.15359 -0.576793 +2.27 -1 -0.00639904 -1.29439 -0.647194 +2.28 -1 -0.0143985 -1.42239 -0.711194 +2.29 -1 -0.025598 -1.53759 -0.768795 +2.3 -1 -0.0399975 -1.63999 -0.819995 +2.31 -1 -0.057597 -1.72959 -0.864796 +2.32 -1 -0.0783965 -1.80639 -0.903197 +2.33 -1 -0.102396 -1.87039 -0.935197 +2.34 -1 -0.129595 -1.9216 -0.960798 +2.35 -1 -0.159995 -1.96 -0.979998 +2.36 -1 -0.193594 -1.9856 -0.992799 +2.37 -1 -0.230394 -1.9984 -0.9992 +2.38 -1 -0.270393 -1.9984 -0.9992 +2.39 -1 -0.313593 -1.9856 -0.992801 +2.4 -1 -0.359992 -1.96 -0.980002 +2.41 -1 -0.409592 -1.9216 -0.960802 +2.42 -1 -0.462391 -1.87041 -0.935203 +2.43 -1 -0.51839 -1.80641 -0.903203 +2.44 -1 -0.57759 -1.72961 -0.864804 +2.45 -1 -0.639989 -1.64001 -0.820004 +2.46 -1 -0.705589 -1.53761 -0.768805 +2.47 -1 -0.774388 -1.42241 -0.711206 +2.48 -1 -0.846387 -1.29441 -0.647206 +2.49 -1 -0.921587 -1.15361 -0.576807 +2.5 -1 -0.999986 -1.00001 -0.500007 +2.51 1 0.921613 1.15359 1.57679 +2.52 1 0.846412 1.29439 1.64719 +2.53 1 0.774412 1.42239 1.71119 +2.54 1 0.705611 1.53759 1.76879 +2.55 1 0.640011 1.63999 1.82 +2.56 1 0.57761 1.72959 1.8648 +2.57 1 0.51841 1.80639 1.9032 +2.58 1 0.462409 1.87039 1.9352 +2.59 1 0.409609 1.9216 1.9608 +2.6 1 0.360008 1.96 1.98 +2.61 1 0.313608 1.9856 1.9928 +2.62 1 0.270407 1.9984 1.9992 +2.63 1 0.230406 1.9984 1.9992 +2.64 1 0.193606 1.9856 1.9928 +2.65 1 0.160005 1.96 1.98 +2.66 1 0.129605 1.9216 1.9608 +2.67 1 0.102404 1.87041 1.9352 +2.68 1 0.0784037 1.80641 1.9032 +2.69 1 0.0576032 1.72961 1.8648 +2.7 1 0.0400026 1.64001 1.82001 +2.71 1 0.0256021 1.53761 1.76881 +2.72 1 0.0144016 1.42241 1.71121 +2.73 1 0.00640106 1.29441 1.64721 +2.74 1 0.0016005 1.15362 1.57681 +2.75 1 0 1.00002 1.50001 +2.76 1 0.00159949 0.846417 1.42321 +2.77 1 0.00639898 0.705615 1.35281 +2.78 1 0.0143985 0.577614 1.28881 +2.79 1 0.0255979 0.462412 1.23121 +2.8 1 0.0399973 0.360011 1.18001 +2.81 1 0.0575968 0.27041 1.1352 +2.82 1 0.0783963 0.193608 1.0968 +2.83 1 0.102396 0.129607 1.0648 +2.84 1 0.129595 0.0784052 1.0392 +2.85 1 0.159995 0.0400037 1.02 +2.86 1 0.193594 0.0144022 1.0072 +2.87 1 0.230393 0.00160074 1.0008 +2.88 1 0.270393 0.00159925 1.0008 +2.89 1 0.313592 0.0143978 1.0072 +2.9 1 0.359992 0.0399963 1.02 +2.91 1 0.409591 0.0783949 1.0392 +2.92 1 0.46239 0.129593 1.0648 +2.93 1 0.51839 0.193592 1.0968 +2.94 1 0.577589 0.270391 1.1352 +2.95 1 0.639988 0.359989 1.17999 +2.96 1 0.705588 0.462388 1.23119 +2.97 1 0.774387 0.577587 1.28879 +2.98 1 0.846387 0.705585 1.35279 +2.99 1 0.921586 0.846384 1.42319 +3 1 0.999985 0.999983 1.49999 +3.01 -1 -0.921614 -0.846416 -0.423208 +3.02 -1 -0.846414 -0.705615 -0.352807 +3.03 -1 -0.774413 -0.577613 -0.288807 +3.04 -1 -0.705613 -0.462412 -0.231206 +3.05 -1 -0.640012 -0.360011 -0.180005 +3.06 -1 -0.577612 -0.270409 -0.135205 +3.07 -1 -0.518411 -0.193608 -0.096804 +3.08 -1 -0.46241 -0.129607 -0.0648033 +3.09 -1 -0.40961 -0.0784052 -0.0392026 +3.1 -1 -0.360009 -0.0400037 -0.0200019 +3.11 -1 -0.313609 -0.0144023 -0.00720114 +3.12 -1 -0.270408 -0.00160074 -0.000800371 +3.13 -1 -0.230408 -0.00159925 -0.000799626 +3.14 -1 -0.193607 -0.0143977 -0.00719884 +3.15 -1 -0.160006 -0.0399961 -0.019998 +3.16 -1 -0.129606 -0.0783945 -0.0391973 +3.17 -1 -0.102405 -0.129593 -0.0647964 +3.18 -1 -0.0784045 -0.193591 -0.0967956 +3.19 -1 -0.0576039 -0.27039 -0.135195 +3.2 -1 -0.0400032 -0.359988 -0.179994 +3.21 -1 -0.0256026 -0.462386 -0.231193 +3.22 -1 -0.014402 -0.577584 -0.288792 +3.23 -1 -0.0064013 -0.705583 -0.352791 +3.24 -1 -0.00160068 -0.846381 -0.42319 +3.25 -1 0 -0.999979 -0.49999 +3.26 -1 -0.00159931 -1.15358 -0.57679 +3.27 -1 -0.00639868 -1.29438 -0.647191 +3.28 -1 -0.014398 -1.42238 -0.711192 +3.29 -1 -0.0255973 -1.53759 -0.768793 +3.3 -1 -0.0399966 -1.63999 -0.819994 +3.31 -1 -0.0575959 -1.72959 -0.864794 +3.32 -1 -0.0783952 -1.80639 -0.903195 +3.33 -1 -0.102394 -1.87039 -0.935196 +3.34 -1 -0.129594 -1.92159 -0.960797 +3.35 -1 -0.159993 -1.96 -0.979998 +3.36 -1 -0.193592 -1.9856 -0.992799 +3.37 -1 -0.230392 -1.9984 -0.9992 +3.38 -1 -0.270391 -1.9984 -0.9992 +3.39 -1 -0.31359 -1.9856 -0.992801 +3.4 -1 -0.359989 -1.96 -0.980002 +3.41 -1 -0.409589 -1.92161 -0.960803 +3.42 -1 -0.462388 -1.87041 -0.935204 +3.43 -1 -0.518387 -1.80641 -0.903205 +3.44 -1 -0.577586 -1.72961 -0.864805 +3.45 -1 -0.639985 -1.64001 -0.820006 +3.46 -1 -0.705585 -1.53761 -0.768807 +3.47 -1 -0.774384 -1.42242 -0.711208 +3.48 -1 -0.846383 -1.29442 -0.647209 +3.49 -1 -0.921582 -1.15362 -0.576809 +3.5 -1 -0.999981 -1.00002 -0.50001 +3.51 1 0.921617 1.15358 1.57679 +3.52 1 0.846417 1.29438 1.64719 +3.53 1 0.774416 1.42238 1.71119 +3.54 1 0.705615 1.53759 1.76879 +3.55 1 0.640015 1.63999 1.81999 +3.56 1 0.577614 1.72959 1.86479 +3.57 1 0.518413 1.80639 1.9032 +3.58 1 0.462412 1.87039 1.9352 +3.59 1 0.409612 1.92159 1.9608 +3.6 1 0.360011 1.96 1.98 +3.61 1 0.31361 1.9856 1.9928 +3.62 1 0.27041 1.9984 1.9992 +3.63 1 0.230409 1.9984 1.9992 +3.64 1 0.193608 1.9856 1.9928 +3.65 1 0.160007 1.96 1.98 +3.66 1 0.129607 1.92161 1.9608 +3.67 1 0.102406 1.87041 1.9352 +3.68 1 0.0784051 1.80641 1.90321 +3.69 1 0.0576044 1.72961 1.86481 +3.7 1 0.0400036 1.64001 1.82001 +3.71 1 0.0256029 1.53762 1.76881 +3.72 1 0.0144022 1.42242 1.71121 +3.73 1 0.00640142 1.29442 1.64721 +3.74 1 0.00160068 1.15362 1.57681 +3.75 1 0 1.00002 1.50001 +3.76 1 0.00159931 0.846422 1.42321 +3.77 1 0.00639856 0.70562 1.35281 +3.78 1 0.0143979 0.577618 1.28881 +3.79 1 0.0255972 0.462416 1.23121 +3.8 1 0.0399964 0.360014 1.18001 +3.81 1 0.0575957 0.270413 1.13521 +3.82 1 0.0783949 0.193611 1.09681 +3.83 1 0.102394 0.129609 1.0648 +3.84 1 0.129593 0.0784068 1.0392 +3.85 1 0.159993 0.0400048 1.02 +3.86 1 0.193592 0.0144029 1.0072 +3.87 1 0.230391 0.00160098 1.0008 +3.88 1 0.27039 0.00159901 1.0008 +3.89 1 0.31359 0.0143971 1.0072 +3.9 1 0.359989 0.0399952 1.02 +3.91 1 0.409588 0.0783933 1.0392 +3.92 1 0.462387 0.129591 1.0648 +3.93 1 0.518386 0.19359 1.09679 +3.94 1 0.577585 0.270388 1.13519 +3.95 1 0.639985 0.359986 1.17999 +3.96 1 0.705584 0.462384 1.23119 +3.97 1 0.774383 0.577582 1.28879 +3.98 1 0.846382 0.70558 1.35279 +3.99 1 0.921581 0.846379 1.42319 +4 1 0.99998 0.999977 1.49999 +4.01 -1 -0.921619 -0.846421 -0.423211 +4.02 -1 -0.846418 -0.705619 -0.35281 +4.03 -1 -0.774417 -0.577618 -0.288809 +4.04 -1 -0.705617 -0.462416 -0.231208 +4.05 -1 -0.640016 -0.360014 -0.180007 +4.06 -1 -0.577615 -0.270412 -0.135206 +4.07 -1 -0.518414 -0.193611 -0.0968053 +4.08 -1 -0.462414 -0.129609 -0.0648043 +4.09 -1 -0.409613 -0.0784068 -0.0392034 +4.1 -1 -0.360012 -0.0400049 -0.0200025 +4.11 -1 -0.313611 -0.0144029 -0.00720146 +4.12 -1 -0.270411 -0.00160098 -0.00080049 +4.13 -1 -0.23041 -0.00159901 -0.000799507 +4.14 -1 -0.193609 -0.014397 -0.00719851 +4.15 -1 -0.160008 -0.039995 -0.0199975 +4.16 -1 -0.129607 -0.0783929 -0.0391965 +4.17 -1 -0.102407 -0.129591 -0.0647954 +4.18 -1 -0.0784059 -0.193589 -0.0967944 +4.19 -1 -0.057605 -0.270387 -0.135193 +4.2 -1 -0.0400042 -0.359984 -0.179992 +4.21 -1 -0.0256034 -0.462382 -0.231191 +4.22 -1 -0.0144026 -0.57758 -0.28879 +4.23 -1 -0.00640172 -0.705578 -0.352789 +4.24 -1 -0.00160086 -0.846376 -0.423188 +4.25 -1 0 -0.999973 -0.499987 +4.26 -1 -0.00159913 -1.15358 -0.576788 +4.27 -1 -0.00639826 -1.29438 -0.647189 +4.28 -1 -0.0143974 -1.42238 -0.71119 +4.29 -1 -0.0255965 -1.53758 -0.768791 +4.3 -1 -0.0399956 -1.63998 -0.819992 +4.31 -1 -0.0575947 -1.72959 -0.864793 +4.32 -1 -0.0783938 -1.80639 -0.903194 +4.33 -1 -0.102393 -1.87039 -0.935195 +4.34 -1 -0.129592 -1.92159 -0.960796 +4.35 -1 -0.159991 -1.95999 -0.979997 +4.36 -1 -0.19359 -1.9856 -0.992798 +4.37 -1 -0.230389 -1.9984 -0.999199 +4.38 -1 -0.270388 -1.9984 -0.999201 +4.39 -1 -0.313587 -1.9856 -0.992802 +4.4 -1 -0.359986 -1.96001 -0.980003 +4.41 -1 -0.409585 -1.92161 -0.960804 +4.42 -1 -0.462385 -1.87041 -0.935205 +4.43 -1 -0.518384 -1.80641 -0.903206 +4.44 -1 -0.577583 -1.72961 -0.864807 +4.45 -1 -0.639982 -1.64002 -0.820008 +4.46 -1 -0.705581 -1.53762 -0.768809 +4.47 -1 -0.77438 -1.42242 -0.71121 +4.48 -1 -0.846379 -1.29442 -0.647211 +4.49 -1 -0.921578 -1.15362 -0.576812 +4.5 -1 -0.999977 -1.00003 -0.500013 +4.51 1 0.921622 1.15358 1.57679 +4.52 1 0.846421 1.29438 1.64719 +4.53 1 0.77442 1.42238 1.71119 +4.54 1 0.705619 1.53758 1.76879 +4.55 1 0.640018 1.63998 1.81999 +4.56 1 0.577617 1.72959 1.86479 +4.57 1 0.518417 1.80639 1.90319 +4.58 1 0.462416 1.87039 1.9352 +4.59 1 0.409615 1.92159 1.9608 +4.6 1 0.360014 1.95999 1.98 +4.61 1 0.313613 1.9856 1.9928 +4.62 1 0.270412 1.9984 1.9992 +4.63 1 0.230411 1.9984 1.9992 +4.64 1 0.19361 1.9856 1.9928 +4.65 1 0.160009 1.96001 1.98 +4.66 1 0.129608 1.92161 1.9608 +4.67 1 0.102407 1.87041 1.93521 +4.68 1 0.0784064 1.80641 1.90321 +4.69 1 0.0576055 1.72961 1.86481 +4.7 1 0.0400046 1.64002 1.82001 +4.71 1 0.0256036 1.53762 1.76881 +4.72 1 0.0144027 1.42242 1.71121 +4.73 1 0.00640184 1.29442 1.64721 +4.74 1 0.00160092 1.15363 1.57681 +4.75 1 0 1.00003 1.50001 +4.76 1 0.00159907 0.846427 1.42321 +4.77 1 0.0063982 0.705625 1.35281 +4.78 1 0.0143973 0.577623 1.28881 +4.79 1 0.0255964 0.46242 1.23121 +4.8 1 0.0399954 0.360018 1.18001 +4.81 1 0.0575945 0.270415 1.13521 +4.82 1 0.0783936 0.193613 1.09681 +4.83 1 0.102393 0.129611 1.06481 +4.84 1 0.129592 0.0784084 1.0392 +4.85 1 0.159991 0.040006 1.02 +4.86 1 0.19359 0.0144036 1.0072 +4.87 1 0.230389 0.00160122 1.0008 +4.88 1 0.270388 0.00159878 1.0008 +4.89 1 0.313587 0.0143964 1.0072 +4.9 1 0.359986 0.0399941 1.02 +4.91 1 0.409585 0.0783917 1.0392 +4.92 1 0.462384 0.129589 1.06479 +4.93 1 0.518383 0.193587 1.09679 +4.94 1 0.577582 0.270385 1.13519 +4.95 1 0.639981 0.359982 1.17999 +4.96 1 0.70558 0.46238 1.23119 +4.97 1 0.774379 0.577578 1.28879 +4.98 1 0.846378 0.705576 1.35279 +4.99 1 0.921577 0.846373 1.42319 diff --git a/tests/lfo/lfo_waves.sfz b/tests/lfo/lfo_waves.sfz new file mode 100644 index 00000000..091245dd --- /dev/null +++ b/tests/lfo/lfo_waves.sfz @@ -0,0 +1,23 @@ + +sample=*sine +lfo1_wave=0 +lfo1_freq=1.0 +lfo2_wave=1 +lfo2_freq=2.0 +lfo2_scale=0.8 +lfo3_wave=2 +lfo3_freq=1.0 +lfo3_scale=0.5 +lfo4_wave=3 +lfo4_freq=2.0 +lfo4_ratio=2 +lfo5_wave=4 +lfo5_freq=1.0 +lfo5_offset=0.5 +lfo6_wave=5 +lfo6_freq=2.0 +lfo7_wave=6 +lfo7_freq=1.0 +lfo7_phase=180 +lfo8_wave=7 +lfo8_freq=2.0 diff --git a/tests/lfo/lfo_waves_reference.dat b/tests/lfo/lfo_waves_reference.dat new file mode 100644 index 00000000..9a65cdf2 --- /dev/null +++ b/tests/lfo/lfo_waves_reference.dat @@ -0,0 +1,500 @@ +0 0 0 0.5 1 1.5 1 0 1 +0.01 0.04 -0.12288 0.5 1 1.5 1 0.02 0.96 +0.02 0.08 -0.23552 0.5 1 1.5 1 0.04 0.92 +0.03 0.12 -0.33792 0.5 1 1.5 1 0.0599999 0.88 +0.04 0.16 -0.43008 0.5 1 1.5 1 0.0799999 0.84 +0.05 0.2 -0.512 0.5 1 1.5 1 0.0999999 0.8 +0.06 0.24 -0.58368 0.5 1 1.5 1 0.12 0.76 +0.07 0.28 -0.64512 0.5 1 1.5 -1 0.14 0.72 +0.08 0.32 -0.69632 0.5 1 1.5 -1 0.16 0.68 +0.09 0.36 -0.73728 0.5 1 1.5 -1 0.18 0.64 +0.1 0.4 -0.768 0.5 1 1.5 -1 0.2 0.6 +0.11 0.44 -0.78848 0.5 1 1.5 -1 0.22 0.56 +0.12 0.48 -0.79872 0.5 1 1.5 -1 0.24 0.52 +0.13 0.52 -0.79872 0.5 -1 1.5 -1 0.26 0.48 +0.14 0.56 -0.78848 0.5 -1 1.5 -1 0.28 0.44 +0.15 0.6 -0.768 0.5 -1 1.5 -1 0.3 0.4 +0.16 0.64 -0.73728 0.5 -1 1.5 -1 0.32 0.36 +0.17 0.68 -0.69632 0.5 -1 1.5 -1 0.34 0.32 +0.18 0.72 -0.64512 0.5 -1 1.5 -1 0.36 0.28 +0.19 0.76 -0.58368 0.5 -1 1.5 -1 0.38 0.24 +0.2 0.8 -0.512 0.5 -1 1.5 -1 0.4 0.2 +0.21 0.84 -0.43008 0.5 -1 1.5 -1 0.42 0.16 +0.22 0.88 -0.33792 0.5 -1 1.5 -1 0.44 0.12 +0.23 0.92 -0.23552 0.5 -1 1.5 -1 0.46 0.0799999 +0.24 0.96 -0.12288 0.5 -1 1.5 -1 0.48 0.0399998 +0.25 1 3.8147e-07 0.5 1 -0.5 -1 0.5 -1.19209e-07 +0.26 0.96 0.12288 0.5 1 -0.5 -1 0.52 -0.0400001 +0.27 0.92 0.23552 0.5 1 -0.5 -1 0.539999 -0.08 +0.28 0.88 0.33792 0.5 1 -0.5 -1 0.559999 -0.12 +0.29 0.84 0.43008 0.5 1 -0.5 -1 0.579999 -0.16 +0.3 0.8 0.512 0.5 1 -0.5 -1 0.599999 -0.2 +0.31 0.76 0.58368 0.5 1 -0.5 -1 0.619999 -0.24 +0.32 0.72 0.64512 0.5 1 -0.5 -1 0.639999 -0.28 +0.33 0.68 0.69632 0.5 1 -0.5 -1 0.659999 -0.32 +0.34 0.64 0.73728 0.5 1 -0.5 -1 0.679999 -0.36 +0.35 0.6 0.768 0.5 1 -0.5 -1 0.699999 -0.4 +0.36 0.56 0.78848 0.5 1 -0.5 -1 0.719999 -0.44 +0.37 0.52 0.79872 0.5 1 -0.5 -1 0.739999 -0.48 +0.38 0.48 0.79872 0.5 -1 -0.5 -1 0.759999 -0.52 +0.39 0.44 0.78848 0.5 -1 -0.5 -1 0.779999 -0.56 +0.4 0.4 0.768 0.5 -1 -0.5 -1 0.799999 -0.6 +0.41 0.36 0.73728 0.5 -1 -0.5 -1 0.819999 -0.64 +0.42 0.320001 0.696321 0.5 -1 -0.5 -1 0.839999 -0.679999 +0.43 0.280001 0.645121 0.5 -1 -0.5 -1 0.859999 -0.719999 +0.44 0.240001 0.583681 0.5 -1 -0.5 -1 0.879999 -0.759999 +0.45 0.200001 0.512001 0.5 -1 -0.5 -1 0.899999 -0.799999 +0.46 0.160001 0.430081 0.5 -1 -0.5 -1 0.919999 -0.839999 +0.47 0.120001 0.337922 0.5 -1 -0.5 -1 0.939999 -0.879999 +0.48 0.0800008 0.235522 0.5 -1 -0.5 -1 0.959999 -0.919999 +0.49 0.0400008 0.122882 0.5 -1 -0.5 -1 0.979999 -0.959999 +0.5 8.34465e-07 2.67029e-06 0.5 1 -0.5 -1 0.999999 -0.999999 +0.51 -0.0399992 -0.122878 0.5 1 -0.5 1 -0.980001 0.960001 +0.52 -0.0799992 -0.235518 0.5 1 -0.5 1 -0.960001 0.920001 +0.53 -0.119999 -0.337918 0.5 1 -0.5 1 -0.940001 0.880001 +0.54 -0.159999 -0.430078 0.5 1 -0.5 1 -0.920001 0.840001 +0.55 -0.199999 -0.511998 0.5 1 -0.5 1 -0.900001 0.800001 +0.56 -0.239999 -0.583679 0.5 1 -0.5 1 -0.880001 0.760001 +0.57 -0.279999 -0.645119 0.5 1 -0.5 -1 -0.860001 0.720001 +0.58 -0.319999 -0.696319 0.5 1 -0.5 -1 -0.840001 0.680001 +0.59 -0.359999 -0.737279 0.5 1 -0.5 -1 -0.820001 0.640001 +0.6 -0.399999 -0.767999 0.5 1 -0.5 -1 -0.800001 0.600001 +0.61 -0.439999 -0.78848 0.5 1 -0.5 -1 -0.780001 0.560001 +0.62 -0.479999 -0.79872 0.5 1 -0.5 -1 -0.760001 0.520001 +0.63 -0.519999 -0.79872 0.5 -1 -0.5 -1 -0.740001 0.480001 +0.64 -0.559999 -0.78848 0.5 -1 -0.5 -1 -0.720001 0.440001 +0.65 -0.599999 -0.768001 0.5 -1 -0.5 -1 -0.700001 0.400001 +0.66 -0.639999 -0.737281 0.5 -1 -0.5 -1 -0.680001 0.360001 +0.67 -0.679999 -0.696321 0.5 -1 -0.5 -1 -0.660001 0.320001 +0.68 -0.719999 -0.645121 0.5 -1 -0.5 -1 -0.640001 0.280001 +0.69 -0.759999 -0.583681 0.5 -1 -0.5 -1 -0.620001 0.240001 +0.7 -0.799999 -0.512001 0.5 -1 -0.5 -1 -0.600001 0.200001 +0.71 -0.839998 -0.430081 0.5 -1 -0.5 -1 -0.580001 0.160001 +0.72 -0.879998 -0.337921 0.5 -1 -0.5 -1 -0.560001 0.120001 +0.73 -0.919998 -0.235522 0.5 -1 -0.5 -1 -0.540001 0.0800006 +0.74 -0.959998 -0.122882 0.5 -1 -0.5 -1 -0.520001 0.0400006 +0.75 -0.999998 -1.71661e-06 0.5 1 -0.5 -1 -0.500001 5.36442e-07 +0.76 -0.960002 0.122878 -0.5 1 -0.5 -1 -0.480001 -0.0399995 +0.77 -0.920002 0.235519 -0.5 1 -0.5 -1 -0.460001 -0.0799994 +0.78 -0.880002 0.337919 -0.5 1 -0.5 -1 -0.440001 -0.119999 +0.79 -0.840002 0.430079 -0.5 1 -0.5 -1 -0.420001 -0.159999 +0.8 -0.800002 0.511999 -0.5 1 -0.5 -1 -0.400001 -0.199999 +0.81 -0.760002 0.583679 -0.5 1 -0.5 -1 -0.380001 -0.239999 +0.82 -0.720002 0.645119 -0.5 1 -0.5 -1 -0.360001 -0.279999 +0.83 -0.680002 0.696319 -0.5 1 -0.5 -1 -0.340001 -0.319999 +0.84 -0.640002 0.737279 -0.5 1 -0.5 -1 -0.320001 -0.359999 +0.85 -0.600002 0.767999 -0.5 1 -0.5 -1 -0.300001 -0.399999 +0.86 -0.560002 0.78848 -0.5 1 -0.5 -1 -0.280001 -0.439999 +0.87 -0.520002 0.79872 -0.5 1 -0.5 -1 -0.260001 -0.479999 +0.88 -0.480002 0.79872 -0.5 -1 -0.5 -1 -0.240001 -0.519999 +0.89 -0.440002 0.78848 -0.5 -1 -0.5 -1 -0.220001 -0.559999 +0.9 -0.400002 0.768001 -0.5 -1 -0.5 -1 -0.200001 -0.599999 +0.91 -0.360002 0.737281 -0.5 -1 -0.5 -1 -0.180001 -0.639999 +0.92 -0.320002 0.696321 -0.5 -1 -0.5 -1 -0.160001 -0.679999 +0.93 -0.280002 0.645122 -0.5 -1 -0.5 -1 -0.140001 -0.719999 +0.94 -0.240002 0.583682 -0.5 -1 -0.5 -1 -0.120001 -0.759999 +0.95 -0.200002 0.512002 -0.5 -1 -0.5 -1 -0.100001 -0.799999 +0.96 -0.160002 0.430083 -0.5 -1 -0.5 -1 -0.0800012 -0.839999 +0.97 -0.120003 0.337923 -0.5 -1 -0.5 -1 -0.0600013 -0.879999 +0.98 -0.0800025 0.235524 -0.5 -1 -0.5 -1 -0.0400013 -0.919999 +0.99 -0.0400026 0.122884 -0.5 -1 -0.5 -1 -0.0200013 -0.959999 +1 -2.6226e-06 4.57763e-06 -0.5 1 -0.5 -1 -1.3113e-06 -0.999999 +1.01 0.0399976 -0.122876 0.5 1 1.5 1 0.0199987 0.960001 +1.02 0.0799976 -0.235516 0.5 1 1.5 1 0.0399987 0.920001 +1.03 0.119998 -0.337916 0.5 1 1.5 1 0.0599986 0.880001 +1.04 0.159998 -0.430077 0.5 1 1.5 1 0.0799986 0.840001 +1.05 0.199998 -0.511997 0.5 1 1.5 1 0.0999986 0.800002 +1.06 0.239998 -0.583678 0.5 1 1.5 1 0.119999 0.760001 +1.07 0.279998 -0.645118 0.5 1 1.5 -1 0.139999 0.720001 +1.08 0.319998 -0.696318 0.5 1 1.5 -1 0.159999 0.680001 +1.09 0.359998 -0.737279 0.5 1 1.5 -1 0.179999 0.640002 +1.1 0.399998 -0.767999 0.5 1 1.5 -1 0.199998 0.600002 +1.11 0.439998 -0.788479 0.5 1 1.5 -1 0.219998 0.560001 +1.12 0.479998 -0.79872 0.5 1 1.5 -1 0.239998 0.520002 +1.13 0.519998 -0.79872 0.5 -1 1.5 -1 0.259998 0.480002 +1.14 0.559998 -0.788481 0.5 -1 1.5 -1 0.279998 0.440001 +1.15 0.599998 -0.768001 0.5 -1 1.5 -1 0.299998 0.400001 +1.16 0.639998 -0.737281 0.5 -1 1.5 -1 0.319998 0.360001 +1.17 0.679998 -0.696322 0.5 -1 1.5 -1 0.339998 0.320001 +1.18 0.719998 -0.645122 0.5 -1 1.5 -1 0.359998 0.280001 +1.19 0.759998 -0.583682 0.5 -1 1.5 -1 0.379998 0.240001 +1.2 0.799998 -0.512003 0.5 -1 1.5 -1 0.399998 0.200001 +1.21 0.839998 -0.430083 0.5 -1 1.5 -1 0.419998 0.160001 +1.22 0.879998 -0.337923 0.5 -1 1.5 -1 0.439998 0.120001 +1.23 0.919998 -0.235523 0.5 -1 1.5 -1 0.459998 0.0800013 +1.24 0.959998 -0.122884 0.5 -1 1.5 -1 0.479998 0.0400013 +1.25 0.999998 -4.00543e-06 0.5 1 1.5 -1 0.499998 1.2517e-06 +1.26 0.960002 0.122876 0.5 1 -0.5 -1 0.519998 -0.0399988 +1.27 0.920002 0.235517 0.5 1 -0.5 -1 0.539998 -0.0799987 +1.28 0.880002 0.337917 0.5 1 -0.5 -1 0.559998 -0.119999 +1.29 0.840002 0.430077 0.5 1 -0.5 -1 0.579998 -0.159999 +1.3 0.800002 0.511997 0.5 1 -0.5 -1 0.599998 -0.199999 +1.31 0.760002 0.583678 0.5 1 -0.5 -1 0.619998 -0.239999 +1.32 0.720002 0.645118 0.5 1 -0.5 -1 0.639998 -0.279999 +1.33 0.680002 0.696318 0.5 1 -0.5 -1 0.659998 -0.319999 +1.34 0.640002 0.737279 0.5 1 -0.5 -1 0.679998 -0.359998 +1.35 0.600003 0.767999 0.5 1 -0.5 -1 0.699998 -0.399998 +1.36 0.560003 0.788479 0.5 1 -0.5 -1 0.719998 -0.439998 +1.37 0.520003 0.79872 0.5 1 -0.5 -1 0.739998 -0.479998 +1.38 0.480003 0.79872 0.5 -1 -0.5 -1 0.759998 -0.519998 +1.39 0.440003 0.788481 0.5 -1 -0.5 -1 0.779998 -0.559998 +1.4 0.400003 0.768001 0.5 -1 -0.5 -1 0.799998 -0.599998 +1.41 0.360003 0.737282 0.5 -1 -0.5 -1 0.819998 -0.639998 +1.42 0.320003 0.696322 0.5 -1 -0.5 -1 0.839998 -0.679998 +1.43 0.280003 0.645123 0.5 -1 -0.5 -1 0.859998 -0.719998 +1.44 0.240003 0.583683 0.5 -1 -0.5 -1 0.879998 -0.759998 +1.45 0.200003 0.512004 0.5 -1 -0.5 -1 0.899998 -0.799998 +1.46 0.160003 0.430084 0.5 -1 -0.5 -1 0.919998 -0.839998 +1.47 0.120003 0.337925 0.5 -1 -0.5 -1 0.939998 -0.879998 +1.48 0.080003 0.235526 0.5 -1 -0.5 -1 0.959998 -0.919998 +1.49 0.0400031 0.122886 0.5 -1 -0.5 -1 0.979998 -0.959998 +1.5 3.09944e-06 6.86644e-06 0.5 1 -0.5 -1 0.999998 -0.999998 +1.51 -0.0399969 -0.122874 0.5 1 -0.5 1 -0.980002 0.960002 +1.52 -0.0799968 -0.235514 0.5 1 -0.5 1 -0.960002 0.920002 +1.53 -0.119997 -0.337915 0.5 1 -0.5 1 -0.940002 0.880002 +1.54 -0.159997 -0.430075 0.5 1 -0.5 1 -0.920002 0.840002 +1.55 -0.199997 -0.511996 0.5 1 -0.5 1 -0.900002 0.800002 +1.56 -0.239997 -0.583676 0.5 1 -0.5 1 -0.880002 0.760002 +1.57 -0.279997 -0.645117 0.5 1 -0.5 -1 -0.860002 0.720002 +1.58 -0.319997 -0.696317 0.5 1 -0.5 -1 -0.840002 0.680002 +1.59 -0.359997 -0.737278 0.5 1 -0.5 -1 -0.820002 0.640002 +1.6 -0.399997 -0.767999 0.5 1 -0.5 -1 -0.800002 0.600002 +1.61 -0.439996 -0.788479 0.5 1 -0.5 -1 -0.780002 0.560002 +1.62 -0.479996 -0.79872 0.5 1 -0.5 -1 -0.760002 0.520002 +1.63 -0.519996 -0.79872 0.5 -1 -0.5 -1 -0.740002 0.480002 +1.64 -0.559996 -0.788481 0.5 -1 -0.5 -1 -0.720002 0.440002 +1.65 -0.599996 -0.768001 0.5 -1 -0.5 -1 -0.700002 0.400002 +1.66 -0.639996 -0.737282 0.5 -1 -0.5 -1 -0.680002 0.360002 +1.67 -0.679996 -0.696322 0.5 -1 -0.5 -1 -0.660002 0.320002 +1.68 -0.719996 -0.645123 0.5 -1 -0.5 -1 -0.640002 0.280002 +1.69 -0.759996 -0.583683 0.5 -1 -0.5 -1 -0.620002 0.240002 +1.7 -0.799996 -0.512004 0.5 -1 -0.5 -1 -0.600002 0.200002 +1.71 -0.839996 -0.430084 0.5 -1 -0.5 -1 -0.580002 0.160002 +1.72 -0.879996 -0.337925 0.5 -1 -0.5 -1 -0.560002 0.120002 +1.73 -0.919996 -0.235525 0.5 -1 -0.5 -1 -0.540002 0.080002 +1.74 -0.959996 -0.122886 0.5 -1 -0.5 -1 -0.520002 0.040002 +1.75 -0.999996 -6.29424e-06 0.5 1 -0.5 -1 -0.500002 1.96695e-06 +1.76 -0.960004 0.122874 -0.5 1 -0.5 -1 -0.480002 -0.0399981 +1.77 -0.920004 0.235515 -0.5 1 -0.5 -1 -0.460002 -0.079998 +1.78 -0.880004 0.337915 -0.5 1 -0.5 -1 -0.440002 -0.119998 +1.79 -0.840004 0.430076 -0.5 1 -0.5 -1 -0.420002 -0.159998 +1.8 -0.800004 0.511996 -0.5 1 -0.5 -1 -0.400002 -0.199998 +1.81 -0.760004 0.583676 -0.5 1 -0.5 -1 -0.380002 -0.239998 +1.82 -0.720004 0.645117 -0.5 1 -0.5 -1 -0.360002 -0.279998 +1.83 -0.680004 0.696317 -0.5 1 -0.5 -1 -0.340002 -0.319998 +1.84 -0.640004 0.737278 -0.5 1 -0.5 -1 -0.320002 -0.359998 +1.85 -0.600004 0.767999 -0.5 1 -0.5 -1 -0.300002 -0.399998 +1.86 -0.560004 0.788479 -0.5 1 -0.5 -1 -0.280002 -0.439998 +1.87 -0.520005 0.79872 -0.5 1 -0.5 -1 -0.260002 -0.479998 +1.88 -0.480005 0.79872 -0.5 -1 -0.5 -1 -0.240002 -0.519998 +1.89 -0.440005 0.788481 -0.5 -1 -0.5 -1 -0.220002 -0.559998 +1.9 -0.400005 0.768002 -0.5 -1 -0.5 -1 -0.200002 -0.599998 +1.91 -0.360005 0.737282 -0.5 -1 -0.5 -1 -0.180002 -0.639997 +1.92 -0.320005 0.696323 -0.5 -1 -0.5 -1 -0.160002 -0.679997 +1.93 -0.280005 0.645124 -0.5 -1 -0.5 -1 -0.140002 -0.719997 +1.94 -0.240005 0.583684 -0.5 -1 -0.5 -1 -0.120002 -0.759997 +1.95 -0.200005 0.512005 -0.5 -1 -0.5 -1 -0.100002 -0.799997 +1.96 -0.160005 0.430086 -0.5 -1 -0.5 -1 -0.0800024 -0.839997 +1.97 -0.120005 0.337927 -0.5 -1 -0.5 -1 -0.0600024 -0.879997 +1.98 -0.0800049 0.235527 -0.5 -1 -0.5 -1 -0.0400025 -0.919997 +1.99 -0.040005 0.122888 -0.5 -1 -0.5 -1 -0.0200025 -0.959997 +2 -5.00679e-06 9.15525e-06 -0.5 1 -0.5 -1 -2.5034e-06 -0.999997 +2.01 0.0399952 -0.122871 0.5 1 1.5 1 0.0199975 0.960003 +2.02 0.0799952 -0.235512 0.5 1 1.5 1 0.0399975 0.920003 +2.03 0.119995 -0.337913 0.5 1 1.5 1 0.0599974 0.880003 +2.04 0.159995 -0.430074 0.5 1 1.5 1 0.0799974 0.840003 +2.05 0.199995 -0.511994 0.5 1 1.5 1 0.0999974 0.800003 +2.06 0.239995 -0.583675 0.5 1 1.5 1 0.119997 0.760003 +2.07 0.279995 -0.645116 0.5 1 1.5 -1 0.139997 0.720003 +2.08 0.319995 -0.696317 0.5 1 1.5 -1 0.159997 0.680003 +2.09 0.359995 -0.737277 0.5 1 1.5 -1 0.179997 0.640003 +2.1 0.399995 -0.767998 0.5 1 1.5 -1 0.199997 0.600003 +2.11 0.439995 -0.788479 0.5 1 1.5 -1 0.219997 0.560003 +2.12 0.479995 -0.79872 0.5 1 1.5 -1 0.239997 0.520003 +2.13 0.519995 -0.79872 0.5 -1 1.5 -1 0.259997 0.480003 +2.14 0.559995 -0.788481 0.5 -1 1.5 -1 0.279997 0.440003 +2.15 0.599995 -0.768002 0.5 -1 1.5 -1 0.299997 0.400003 +2.16 0.639995 -0.737283 0.5 -1 1.5 -1 0.319997 0.360003 +2.17 0.679995 -0.696323 0.5 -1 1.5 -1 0.339997 0.320003 +2.18 0.719995 -0.645124 0.5 -1 1.5 -1 0.359997 0.280003 +2.19 0.759995 -0.583685 0.5 -1 1.5 -1 0.379997 0.240003 +2.2 0.799995 -0.512005 0.5 -1 1.5 -1 0.399997 0.200003 +2.21 0.839995 -0.430086 0.5 -1 1.5 -1 0.419997 0.160003 +2.22 0.879995 -0.337927 0.5 -1 1.5 -1 0.439997 0.120003 +2.23 0.919995 -0.235527 0.5 -1 1.5 -1 0.459997 0.0800027 +2.24 0.959995 -0.122888 0.5 -1 1.5 -1 0.479997 0.0400027 +2.25 0.999995 -8.58305e-06 0.5 1 1.5 -1 0.499997 2.68221e-06 +2.26 0.960005 0.122872 0.5 1 -0.5 -1 0.519997 -0.0399973 +2.27 0.920005 0.235513 0.5 1 -0.5 -1 0.539997 -0.0799973 +2.28 0.880005 0.337913 0.5 1 -0.5 -1 0.559997 -0.119997 +2.29 0.840005 0.430074 0.5 1 -0.5 -1 0.579997 -0.159997 +2.3 0.800005 0.511995 0.5 1 -0.5 -1 0.599997 -0.199997 +2.31 0.760005 0.583675 0.5 1 -0.5 -1 0.619997 -0.239997 +2.32 0.720005 0.645116 0.5 1 -0.5 -1 0.639997 -0.279997 +2.33 0.680005 0.696317 0.5 1 -0.5 -1 0.659997 -0.319997 +2.34 0.640005 0.737277 0.5 1 -0.5 -1 0.679997 -0.359997 +2.35 0.600005 0.767998 0.5 1 -0.5 -1 0.699997 -0.399997 +2.36 0.560005 0.788479 0.5 1 -0.5 -1 0.719997 -0.439997 +2.37 0.520005 0.79872 0.5 1 -0.5 -1 0.739997 -0.479997 +2.38 0.480005 0.79872 0.5 -1 -0.5 -1 0.759997 -0.519997 +2.39 0.440005 0.788481 0.5 -1 -0.5 -1 0.779997 -0.559997 +2.4 0.400005 0.768002 0.5 -1 -0.5 -1 0.799997 -0.599997 +2.41 0.360005 0.737283 0.5 -1 -0.5 -1 0.819997 -0.639997 +2.42 0.320005 0.696324 0.5 -1 -0.5 -1 0.839997 -0.679997 +2.43 0.280005 0.645125 0.5 -1 -0.5 -1 0.859997 -0.719997 +2.44 0.240005 0.583686 0.5 -1 -0.5 -1 0.879997 -0.759997 +2.45 0.200005 0.512007 0.5 -1 -0.5 -1 0.899997 -0.799997 +2.46 0.160005 0.430087 0.5 -1 -0.5 -1 0.919997 -0.839997 +2.47 0.120005 0.337928 0.5 -1 -0.5 -1 0.939997 -0.879997 +2.48 0.0800054 0.235529 0.5 -1 -0.5 -1 0.959997 -0.919997 +2.49 0.0400054 0.12289 0.5 -1 -0.5 -1 0.979997 -0.959996 +2.5 5.48363e-06 1.14441e-05 0.5 1 -0.5 -1 0.999997 -0.999996 +2.51 -0.0399945 -0.122869 0.5 1 -0.5 1 -0.980003 0.960004 +2.52 -0.0799944 -0.23551 0.5 1 -0.5 1 -0.960003 0.920004 +2.53 -0.119994 -0.337911 0.5 1 -0.5 1 -0.940003 0.880004 +2.54 -0.159994 -0.430072 0.5 1 -0.5 1 -0.920003 0.840004 +2.55 -0.199994 -0.511993 0.5 1 -0.5 1 -0.900003 0.800004 +2.56 -0.239994 -0.583674 0.5 1 -0.5 1 -0.880003 0.760004 +2.57 -0.279994 -0.645115 0.5 1 -0.5 -1 -0.860003 0.720004 +2.58 -0.319994 -0.696316 0.5 1 -0.5 -1 -0.840003 0.680004 +2.59 -0.359994 -0.737277 0.5 1 -0.5 -1 -0.820003 0.640004 +2.6 -0.399994 -0.767998 0.5 1 -0.5 -1 -0.800003 0.600004 +2.61 -0.439994 -0.788479 0.5 1 -0.5 -1 -0.780003 0.560004 +2.62 -0.479994 -0.79872 0.5 1 -0.5 -1 -0.760003 0.520004 +2.63 -0.519994 -0.79872 0.5 -1 -0.5 -1 -0.740003 0.480004 +2.64 -0.559994 -0.788481 0.5 -1 -0.5 -1 -0.720003 0.440004 +2.65 -0.599994 -0.768002 0.5 -1 -0.5 -1 -0.700003 0.400004 +2.66 -0.639994 -0.737283 0.5 -1 -0.5 -1 -0.680003 0.360004 +2.67 -0.679994 -0.696324 0.5 -1 -0.5 -1 -0.660003 0.320004 +2.68 -0.719994 -0.645125 0.5 -1 -0.5 -1 -0.640003 0.280004 +2.69 -0.759994 -0.583686 0.5 -1 -0.5 -1 -0.620003 0.240004 +2.7 -0.799994 -0.512007 0.5 -1 -0.5 -1 -0.600003 0.200004 +2.71 -0.839994 -0.430088 0.5 -1 -0.5 -1 -0.580003 0.160003 +2.72 -0.879994 -0.337928 0.5 -1 -0.5 -1 -0.560003 0.120003 +2.73 -0.919994 -0.235529 0.5 -1 -0.5 -1 -0.540003 0.0800034 +2.74 -0.959994 -0.12289 0.5 -1 -0.5 -1 -0.520003 0.0400034 +2.75 -0.999994 -1.08719e-05 0.5 1 -0.5 -1 -0.500003 3.39746e-06 +2.76 -0.960006 0.12287 -0.5 1 -0.5 -1 -0.480003 -0.0399966 +2.77 -0.920007 0.235511 -0.5 1 -0.5 -1 -0.460003 -0.0799966 +2.78 -0.880007 0.337912 -0.5 1 -0.5 -1 -0.440003 -0.119997 +2.79 -0.840007 0.430072 -0.5 1 -0.5 -1 -0.420003 -0.159997 +2.8 -0.800007 0.511993 -0.5 1 -0.5 -1 -0.400003 -0.199996 +2.81 -0.760007 0.583674 -0.5 1 -0.5 -1 -0.380003 -0.239996 +2.82 -0.720007 0.645115 -0.5 1 -0.5 -1 -0.360003 -0.279996 +2.83 -0.680007 0.696316 -0.5 1 -0.5 -1 -0.340003 -0.319996 +2.84 -0.640007 0.737277 -0.5 1 -0.5 -1 -0.320003 -0.359996 +2.85 -0.600007 0.767998 -0.5 1 -0.5 -1 -0.300003 -0.399996 +2.86 -0.560007 0.788479 -0.5 1 -0.5 -1 -0.280003 -0.439996 +2.87 -0.520007 0.79872 -0.5 1 -0.5 -1 -0.260003 -0.479996 +2.88 -0.480007 0.79872 -0.5 -1 -0.5 -1 -0.240003 -0.519996 +2.89 -0.440007 0.788482 -0.5 -1 -0.5 -1 -0.220003 -0.559996 +2.9 -0.400007 0.768003 -0.5 -1 -0.5 -1 -0.200004 -0.599996 +2.91 -0.360007 0.737284 -0.5 -1 -0.5 -1 -0.180004 -0.639996 +2.92 -0.320007 0.696325 -0.5 -1 -0.5 -1 -0.160004 -0.679996 +2.93 -0.280007 0.645126 -0.5 -1 -0.5 -1 -0.140004 -0.719996 +2.94 -0.240007 0.583687 -0.5 -1 -0.5 -1 -0.120004 -0.759996 +2.95 -0.200007 0.512008 -0.5 -1 -0.5 -1 -0.100004 -0.799996 +2.96 -0.160007 0.430089 -0.5 -1 -0.5 -1 -0.0800036 -0.839996 +2.97 -0.120007 0.33793 -0.5 -1 -0.5 -1 -0.0600036 -0.879996 +2.98 -0.0800073 0.235531 -0.5 -1 -0.5 -1 -0.0400037 -0.919996 +2.99 -0.0400074 0.122893 -0.5 -1 -0.5 -1 -0.0200037 -0.959996 +3 -7.39098e-06 1.37329e-05 -0.5 1 -0.5 -1 -3.69549e-06 -0.999996 +3.01 0.0399928 -0.122867 0.5 1 1.5 1 0.0199963 0.960004 +3.02 0.0799928 -0.235508 0.5 1 1.5 1 0.0399963 0.920004 +3.03 0.119993 -0.337909 0.5 1 1.5 1 0.0599962 0.880004 +3.04 0.159993 -0.430071 0.5 1 1.5 1 0.0799962 0.840004 +3.05 0.199993 -0.511992 0.5 1 1.5 1 0.0999962 0.800004 +3.06 0.239993 -0.583673 0.5 1 1.5 1 0.119996 0.760004 +3.07 0.279993 -0.645114 0.5 1 1.5 -1 0.139996 0.720004 +3.08 0.319993 -0.696315 0.5 1 1.5 -1 0.159996 0.680004 +3.09 0.359993 -0.737276 0.5 1 1.5 -1 0.179996 0.640004 +3.1 0.399993 -0.767997 0.5 1 1.5 -1 0.199996 0.600004 +3.11 0.439993 -0.788478 0.5 1 1.5 -1 0.219996 0.560004 +3.12 0.479993 -0.798719 0.5 1 1.5 -1 0.239996 0.520004 +3.13 0.519993 -0.798721 0.5 -1 1.5 -1 0.259996 0.480004 +3.14 0.559993 -0.788482 0.5 -1 1.5 -1 0.279996 0.440004 +3.15 0.599993 -0.768003 0.5 -1 1.5 -1 0.299996 0.400004 +3.16 0.639993 -0.737284 0.5 -1 1.5 -1 0.319996 0.360004 +3.17 0.679993 -0.696325 0.5 -1 1.5 -1 0.339996 0.320004 +3.18 0.719993 -0.645126 0.5 -1 1.5 -1 0.359996 0.280004 +3.19 0.759993 -0.583687 0.5 -1 1.5 -1 0.379996 0.240004 +3.2 0.799993 -0.512008 0.5 -1 1.5 -1 0.399996 0.200004 +3.21 0.839993 -0.430089 0.5 -1 1.5 -1 0.419996 0.160004 +3.22 0.879993 -0.33793 0.5 -1 1.5 -1 0.439996 0.120004 +3.23 0.919993 -0.235531 0.5 -1 1.5 -1 0.459996 0.0800042 +3.24 0.959993 -0.122892 0.5 -1 1.5 -1 0.479996 0.0400041 +3.25 0.999993 -1.31607e-05 0.5 1 1.5 -1 0.499996 4.11272e-06 +3.26 0.960007 0.122868 0.5 1 -0.5 -1 0.519996 -0.0399959 +3.27 0.920007 0.235509 0.5 1 -0.5 -1 0.539996 -0.0799959 +3.28 0.880007 0.33791 0.5 1 -0.5 -1 0.559996 -0.119996 +3.29 0.840007 0.430071 0.5 1 -0.5 -1 0.579996 -0.159996 +3.3 0.800007 0.511992 0.5 1 -0.5 -1 0.599996 -0.199996 +3.31 0.760007 0.583673 0.5 1 -0.5 -1 0.619996 -0.239996 +3.32 0.720007 0.645114 0.5 1 -0.5 -1 0.639996 -0.279996 +3.33 0.680007 0.696315 0.5 1 -0.5 -1 0.659996 -0.319996 +3.34 0.640007 0.737276 0.5 1 -0.5 -1 0.679996 -0.359996 +3.35 0.600007 0.767997 0.5 1 -0.5 -1 0.699996 -0.399996 +3.36 0.560007 0.788478 0.5 1 -0.5 -1 0.719996 -0.439996 +3.37 0.520007 0.798719 0.5 1 -0.5 -1 0.739996 -0.479995 +3.38 0.480007 0.798721 0.5 -1 -0.5 -1 0.759996 -0.519995 +3.39 0.440007 0.788482 0.5 -1 -0.5 -1 0.779996 -0.559995 +3.4 0.400007 0.768003 0.5 -1 -0.5 -1 0.799996 -0.599995 +3.41 0.360008 0.737284 0.5 -1 -0.5 -1 0.819996 -0.639995 +3.42 0.320008 0.696325 0.5 -1 -0.5 -1 0.839996 -0.679995 +3.43 0.280008 0.645127 0.5 -1 -0.5 -1 0.859995 -0.719995 +3.44 0.240008 0.583688 0.5 -1 -0.5 -1 0.879995 -0.759995 +3.45 0.200008 0.512009 0.5 -1 -0.5 -1 0.899995 -0.799995 +3.46 0.160008 0.430091 0.5 -1 -0.5 -1 0.919995 -0.839995 +3.47 0.120008 0.337932 0.5 -1 -0.5 -1 0.939995 -0.879995 +3.48 0.0800078 0.235533 0.5 -1 -0.5 -1 0.959995 -0.919995 +3.49 0.0400078 0.122895 0.5 -1 -0.5 -1 0.979995 -0.959995 +3.5 7.86781e-06 1.60216e-05 0.5 1 -0.5 -1 0.999995 -0.999995 +3.51 -0.0399921 -0.122865 0.5 1 -0.5 1 -0.980005 0.960005 +3.52 -0.0799921 -0.235507 0.5 1 -0.5 1 -0.960005 0.920005 +3.53 -0.119992 -0.337908 0.5 1 -0.5 1 -0.940005 0.880005 +3.54 -0.159992 -0.430069 0.5 1 -0.5 1 -0.920005 0.840005 +3.55 -0.199992 -0.51199 0.5 1 -0.5 1 -0.900005 0.800005 +3.56 -0.239992 -0.583672 0.5 1 -0.5 1 -0.880005 0.760005 +3.57 -0.279992 -0.645113 0.5 1 -0.5 -1 -0.860005 0.720005 +3.58 -0.319992 -0.696314 0.5 1 -0.5 -1 -0.840005 0.680005 +3.59 -0.359992 -0.737275 0.5 1 -0.5 -1 -0.820005 0.640005 +3.6 -0.399992 -0.767997 0.5 1 -0.5 -1 -0.800005 0.600005 +3.61 -0.439992 -0.788478 0.5 1 -0.5 -1 -0.780005 0.560005 +3.62 -0.479992 -0.798719 0.5 1 -0.5 -1 -0.760005 0.520005 +3.63 -0.519992 -0.798721 0.5 -1 -0.5 -1 -0.740005 0.480005 +3.64 -0.559992 -0.788482 0.5 -1 -0.5 -1 -0.720005 0.440005 +3.65 -0.599992 -0.768003 0.5 -1 -0.5 -1 -0.700005 0.400005 +3.66 -0.639992 -0.737284 0.5 -1 -0.5 -1 -0.680005 0.360005 +3.67 -0.679991 -0.696326 0.5 -1 -0.5 -1 -0.660004 0.320005 +3.68 -0.719991 -0.645127 0.5 -1 -0.5 -1 -0.640005 0.280005 +3.69 -0.759991 -0.583688 0.5 -1 -0.5 -1 -0.620005 0.240005 +3.7 -0.799991 -0.512009 0.5 -1 -0.5 -1 -0.600004 0.200005 +3.71 -0.839991 -0.430091 0.5 -1 -0.5 -1 -0.580004 0.160005 +3.72 -0.879991 -0.337932 0.5 -1 -0.5 -1 -0.560004 0.120005 +3.73 -0.919991 -0.235533 0.5 -1 -0.5 -1 -0.540004 0.0800049 +3.74 -0.959991 -0.122894 0.5 -1 -0.5 -1 -0.520004 0.0400048 +3.75 -0.999991 -1.54495e-05 0.5 1 -0.5 -1 -0.500004 4.82798e-06 +3.76 -0.960009 0.122866 -0.5 1 -0.5 -1 -0.480004 -0.0399952 +3.77 -0.920009 0.235507 -0.5 1 -0.5 -1 -0.460004 -0.0799952 +3.78 -0.880009 0.337908 -0.5 1 -0.5 -1 -0.440004 -0.119995 +3.79 -0.840009 0.430069 -0.5 1 -0.5 -1 -0.420004 -0.159995 +3.8 -0.800009 0.51199 -0.5 1 -0.5 -1 -0.400005 -0.199995 +3.81 -0.760009 0.583672 -0.5 1 -0.5 -1 -0.380005 -0.239995 +3.82 -0.720009 0.645113 -0.5 1 -0.5 -1 -0.360005 -0.279995 +3.83 -0.680009 0.696314 -0.5 1 -0.5 -1 -0.340005 -0.319995 +3.84 -0.640009 0.737275 -0.5 1 -0.5 -1 -0.320005 -0.359995 +3.85 -0.600009 0.767997 -0.5 1 -0.5 -1 -0.300005 -0.399995 +3.86 -0.560009 0.788478 -0.5 1 -0.5 -1 -0.280005 -0.439995 +3.87 -0.520009 0.798719 -0.5 1 -0.5 -1 -0.260005 -0.479995 +3.88 -0.480009 0.798721 -0.5 -1 -0.5 -1 -0.240005 -0.519995 +3.89 -0.440009 0.788482 -0.5 -1 -0.5 -1 -0.220005 -0.559995 +3.9 -0.400009 0.768003 -0.5 -1 -0.5 -1 -0.200005 -0.599995 +3.91 -0.360009 0.737285 -0.5 -1 -0.5 -1 -0.180005 -0.639995 +3.92 -0.320009 0.696326 -0.5 -1 -0.5 -1 -0.160005 -0.679995 +3.93 -0.28001 0.645128 -0.5 -1 -0.5 -1 -0.140005 -0.719995 +3.94 -0.24001 0.583689 -0.5 -1 -0.5 -1 -0.120005 -0.759995 +3.95 -0.20001 0.512011 -0.5 -1 -0.5 -1 -0.100005 -0.799994 +3.96 -0.16001 0.430092 -0.5 -1 -0.5 -1 -0.0800048 -0.839994 +3.97 -0.12001 0.337934 -0.5 -1 -0.5 -1 -0.0600048 -0.879994 +3.98 -0.0800097 0.235535 -0.5 -1 -0.5 -1 -0.0400048 -0.919994 +3.99 -0.0400097 0.122897 -0.5 -1 -0.5 -1 -0.0200049 -0.959994 +4 -9.77516e-06 1.83104e-05 -0.5 1 -0.5 -1 -4.88758e-06 -0.999994 +4.01 0.0399904 -0.122863 0.5 1 1.5 1 0.0199951 0.960006 +4.02 0.0799904 -0.235505 0.5 1 1.5 1 0.0399951 0.920006 +4.03 0.11999 -0.337906 0.5 1 1.5 1 0.0599951 0.880006 +4.04 0.15999 -0.430068 0.5 1 1.5 1 0.079995 0.840006 +4.05 0.19999 -0.511989 0.5 1 1.5 1 0.099995 0.800006 +4.06 0.23999 -0.58367 0.5 1 1.5 1 0.119995 0.760006 +4.07 0.27999 -0.645112 0.5 1 1.5 -1 0.139995 0.720006 +4.08 0.31999 -0.696313 0.5 1 1.5 -1 0.159995 0.680006 +4.09 0.35999 -0.737275 0.5 1 1.5 -1 0.179995 0.640006 +4.1 0.39999 -0.767996 0.5 1 1.5 -1 0.199995 0.600006 +4.11 0.43999 -0.788478 0.5 1 1.5 -1 0.219995 0.560006 +4.12 0.47999 -0.798719 0.5 1 1.5 -1 0.239995 0.520006 +4.13 0.51999 -0.798721 0.5 -1 1.5 -1 0.259995 0.480006 +4.14 0.55999 -0.788482 0.5 -1 1.5 -1 0.279995 0.440006 +4.15 0.59999 -0.768004 0.5 -1 1.5 -1 0.299995 0.400006 +4.16 0.63999 -0.737285 0.5 -1 1.5 -1 0.319995 0.360006 +4.17 0.67999 -0.696327 0.5 -1 1.5 -1 0.339995 0.320006 +4.18 0.71999 -0.645128 0.5 -1 1.5 -1 0.359995 0.280006 +4.19 0.759991 -0.583689 0.5 -1 1.5 -1 0.379995 0.240006 +4.2 0.799991 -0.512011 0.5 -1 1.5 -1 0.399995 0.200006 +4.21 0.839991 -0.430092 0.5 -1 1.5 -1 0.419995 0.160006 +4.22 0.879991 -0.337934 0.5 -1 1.5 -1 0.439995 0.120006 +4.23 0.919991 -0.235535 0.5 -1 1.5 -1 0.459995 0.0800056 +4.24 0.959991 -0.122896 0.5 -1 1.5 -1 0.479995 0.0400056 +4.25 0.999991 -1.77382e-05 0.5 1 1.5 -1 0.499995 5.54323e-06 +4.26 0.960009 0.122864 0.5 1 -0.5 -1 0.519995 -0.0399945 +4.27 0.920009 0.235505 0.5 1 -0.5 -1 0.539995 -0.0799944 +4.28 0.880009 0.337906 0.5 1 -0.5 -1 0.559995 -0.119994 +4.29 0.840009 0.430068 0.5 1 -0.5 -1 0.579995 -0.159994 +4.3 0.800009 0.511989 0.5 1 -0.5 -1 0.599995 -0.199994 +4.31 0.76001 0.58367 0.5 1 -0.5 -1 0.619995 -0.239994 +4.32 0.72001 0.645112 0.5 1 -0.5 -1 0.639995 -0.279994 +4.33 0.68001 0.696313 0.5 1 -0.5 -1 0.659994 -0.319994 +4.34 0.64001 0.737275 0.5 1 -0.5 -1 0.679994 -0.359994 +4.35 0.60001 0.767996 0.5 1 -0.5 -1 0.699994 -0.399994 +4.36 0.56001 0.788478 0.5 1 -0.5 -1 0.719994 -0.439994 +4.37 0.52001 0.798719 0.5 1 -0.5 -1 0.739994 -0.479994 +4.38 0.48001 0.798721 0.5 -1 -0.5 -1 0.759994 -0.519994 +4.39 0.44001 0.788482 0.5 -1 -0.5 -1 0.779994 -0.559994 +4.4 0.40001 0.768004 0.5 -1 -0.5 -1 0.799994 -0.599994 +4.41 0.36001 0.737285 0.5 -1 -0.5 -1 0.819994 -0.639994 +4.42 0.32001 0.696327 0.5 -1 -0.5 -1 0.839994 -0.679994 +4.43 0.28001 0.645129 0.5 -1 -0.5 -1 0.859994 -0.719994 +4.44 0.24001 0.58369 0.5 -1 -0.5 -1 0.879994 -0.759994 +4.45 0.20001 0.512012 0.5 -1 -0.5 -1 0.899994 -0.799994 +4.46 0.16001 0.430094 0.5 -1 -0.5 -1 0.919994 -0.839994 +4.47 0.12001 0.337935 0.5 -1 -0.5 -1 0.939994 -0.879994 +4.48 0.0800102 0.235537 0.5 -1 -0.5 -1 0.959994 -0.919994 +4.49 0.0400102 0.122899 0.5 -1 -0.5 -1 0.979994 -0.959994 +4.5 1.0252e-05 2.05992e-05 0.5 1 -0.5 -1 0.999994 -0.999994 +4.51 -0.0399897 -0.122861 0.5 1 -0.5 1 -0.980006 0.960006 +4.52 -0.0799897 -0.235503 0.5 1 -0.5 1 -0.960006 0.920006 +4.53 -0.11999 -0.337904 0.5 1 -0.5 1 -0.940006 0.880006 +4.54 -0.15999 -0.430066 0.5 1 -0.5 1 -0.920006 0.840006 +4.55 -0.19999 -0.511988 0.5 1 -0.5 1 -0.900006 0.800007 +4.56 -0.23999 -0.583669 0.5 1 -0.5 1 -0.880006 0.760006 +4.57 -0.279989 -0.645111 0.5 1 -0.5 -1 -0.860006 0.720006 +4.58 -0.319989 -0.696313 0.5 1 -0.5 -1 -0.840006 0.680007 +4.59 -0.359989 -0.737274 0.5 1 -0.5 -1 -0.820006 0.640007 +4.6 -0.399989 -0.767996 0.5 1 -0.5 -1 -0.800006 0.600007 +4.61 -0.439989 -0.788477 0.5 1 -0.5 -1 -0.780006 0.560006 +4.62 -0.479989 -0.798719 0.5 1 -0.5 -1 -0.760006 0.520007 +4.63 -0.519989 -0.798721 0.5 -1 -0.5 -1 -0.740006 0.480007 +4.64 -0.559989 -0.788483 0.5 -1 -0.5 -1 -0.720006 0.440006 +4.65 -0.599989 -0.768004 0.5 -1 -0.5 -1 -0.700006 0.400006 +4.66 -0.639989 -0.737286 0.5 -1 -0.5 -1 -0.680006 0.360006 +4.67 -0.679989 -0.696327 0.5 -1 -0.5 -1 -0.660006 0.320006 +4.68 -0.719989 -0.645129 0.5 -1 -0.5 -1 -0.640006 0.280006 +4.69 -0.759989 -0.583691 0.5 -1 -0.5 -1 -0.620006 0.240006 +4.7 -0.799989 -0.512012 0.5 -1 -0.5 -1 -0.600006 0.200006 +4.71 -0.839989 -0.430094 0.5 -1 -0.5 -1 -0.580006 0.160006 +4.72 -0.879989 -0.337935 0.5 -1 -0.5 -1 -0.560006 0.120006 +4.73 -0.919989 -0.235537 0.5 -1 -0.5 -1 -0.540006 0.0800063 +4.74 -0.959989 -0.122898 0.5 -1 -0.5 -1 -0.520006 0.0400063 +4.75 -0.999989 -2.0027e-05 0.5 1 -0.5 -1 -0.500006 6.25849e-06 +4.76 -0.960011 0.122862 -0.5 1 -0.5 -1 -0.480006 -0.0399938 +4.77 -0.920011 0.235503 -0.5 1 -0.5 -1 -0.460006 -0.0799937 +4.78 -0.880011 0.337905 -0.5 1 -0.5 -1 -0.440006 -0.119994 +4.79 -0.840011 0.430066 -0.5 1 -0.5 -1 -0.420006 -0.159994 +4.8 -0.800011 0.511988 -0.5 1 -0.5 -1 -0.400006 -0.199994 +4.81 -0.760011 0.583669 -0.5 1 -0.5 -1 -0.380006 -0.239994 +4.82 -0.720011 0.645111 -0.5 1 -0.5 -1 -0.360006 -0.279994 +4.83 -0.680012 0.696312 -0.5 1 -0.5 -1 -0.340006 -0.319993 +4.84 -0.640012 0.737274 -0.5 1 -0.5 -1 -0.320006 -0.359993 +4.85 -0.600012 0.767996 -0.5 1 -0.5 -1 -0.300006 -0.399993 +4.86 -0.560012 0.788477 -0.5 1 -0.5 -1 -0.280006 -0.439993 +4.87 -0.520012 0.798719 -0.5 1 -0.5 -1 -0.260006 -0.479993 +4.88 -0.480012 0.798721 -0.5 -1 -0.5 -1 -0.240006 -0.519993 +4.89 -0.440012 0.788483 -0.5 -1 -0.5 -1 -0.220006 -0.559993 +4.9 -0.400012 0.768004 -0.5 -1 -0.5 -1 -0.200006 -0.599993 +4.91 -0.360012 0.737286 -0.5 -1 -0.5 -1 -0.180006 -0.639993 +4.92 -0.320012 0.696328 -0.5 -1 -0.5 -1 -0.160006 -0.679993 +4.93 -0.280012 0.64513 -0.5 -1 -0.5 -1 -0.140006 -0.719993 +4.94 -0.240012 0.583692 -0.5 -1 -0.5 -1 -0.120006 -0.759993 +4.95 -0.200012 0.512013 -0.5 -1 -0.5 -1 -0.100006 -0.799993 +4.96 -0.160012 0.430095 -0.5 -1 -0.5 -1 -0.080006 -0.839993 +4.97 -0.120012 0.337937 -0.5 -1 -0.5 -1 -0.060006 -0.879993 +4.98 -0.0800121 0.235539 -0.5 -1 -0.5 -1 -0.040006 -0.919993 +4.99 -0.0400121 0.122901 -0.5 -1 -0.5 -1 -0.0200061 -0.959993 diff --git a/tests/lfo/plot_lfo.py b/tests/lfo/plot_lfo.py new file mode 100755 index 00000000..34c03df8 --- /dev/null +++ b/tests/lfo/plot_lfo.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +import numpy as np +import matplotlib.pyplot as plt +from argparse import ArgumentParser +import os + +parser = ArgumentParser(usage="Compare 2 files as outputted by sfizz_plot_lfo") +parser.add_argument("file", help="The file to test") +parser.add_argument("reference", help="The reference file") +args = parser.parse_args() + +assert os.path.exists(args.file), "The file to test does not exist" +assert os.path.exists(args.reference), "The reference file does not exist" + +reference_data = np.loadtxt(args.reference) +data = np.loadtxt(args.file) + +assert reference_data.shape == data.shape, "The shapes of the data and reference are different" + +n_samples, n_lfos = reference_data.shape +n_lfos -= 1 # The first column is the time + +if n_lfos == 1: + fig, ax = plt.subplots(figsize=(20, 10)) + ax.plot(reference_data[:, 0], reference_data[:, 1]) + ax.plot(data[:, 0], data[:, 1]) + ax.grid() +elif n_lfos > 4: + n_cols = 4 + n_rows = n_lfos // n_cols + print(n_rows, n_cols) + fig, ax = plt.subplots(n_rows, n_cols, figsize=(20, 10)) + for i in range(n_rows): + for j in range(n_cols): + lfo_index = 1 + i * 4 + j + ax[i, j].plot(reference_data[:, 0], reference_data[:, lfo_index]) + ax[i, j].plot(data[:, 0], data[:, lfo_index]) + ax[i, j].set_title("LFO {}".format(lfo_index)) + ax[i, j].grid() +else: + fig, ax = plt.subplots(1, n_lfos, figsize=(20, 10)) + for i in range(n_lfos): + lfo_index = i + 1 + ax[i].plot(reference_data[:, 0], reference_data[:, lfo_index]) + ax[i].plot(data[:, 0], data[:, lfo_index]) + ax[i].set_title("LFO {}".format(lfo_index)) + ax[i].grid() + +plt.show() From 6cc160b9ff65925552624ed119d826504efd890f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 13 Aug 2020 18:38:13 +0200 Subject: [PATCH 111/445] Move template functions of Opcode.h to implementation side --- src/sfizz/Opcode.cpp | 148 +++++++++++++++++++++++++++++++++++++++++-- src/sfizz/Opcode.h | 104 +++--------------------------- 2 files changed, 151 insertions(+), 101 deletions(-) diff --git a/src/sfizz/Opcode.cpp b/src/sfizz/Opcode.cpp index 52b3ec66..6ec196a2 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -13,7 +13,9 @@ #include #include -sfz::Opcode::Opcode(absl::string_view inputOpcode, absl::string_view inputValue) +namespace sfz { + +Opcode::Opcode(absl::string_view inputOpcode, absl::string_view inputValue) : opcode(trim(inputOpcode)) , value(trim(inputValue)) , category(identifyCategory(inputOpcode)) @@ -49,7 +51,7 @@ static absl::string_view extractBackInteger(absl::string_view opcodeName) return opcodeName.substr(i); } -std::string sfz::Opcode::getDerivedName(sfz::OpcodeCategory newCategory, unsigned number) const +std::string Opcode::getDerivedName(OpcodeCategory newCategory, unsigned number) const { std::string derivedName(opcode); @@ -95,9 +97,9 @@ std::string sfz::Opcode::getDerivedName(sfz::OpcodeCategory newCategory, unsigne return derivedName; } -sfz::OpcodeCategory sfz::Opcode::identifyCategory(absl::string_view name) +OpcodeCategory Opcode::identifyCategory(absl::string_view name) { - sfz::OpcodeCategory category = kOpcodeNormal; + OpcodeCategory category = kOpcodeNormal; if (!name.empty() && absl::ascii_isdigit(name.back())) { absl::string_view part = name; @@ -115,7 +117,7 @@ sfz::OpcodeCategory sfz::Opcode::identifyCategory(absl::string_view name) return category; } -absl::optional sfz::readNoteValue(absl::string_view value) +absl::optional readNoteValue(absl::string_view value) { char noteLetter = absl::ascii_tolower(value.empty() ? '\0' : value.front()); value.remove_prefix(1); @@ -156,6 +158,142 @@ absl::optional sfz::readNoteValue(absl::string_view value) return static_cast(noteNumber); } +/// +template ::value, int>> +absl::optional readOpcode(absl::string_view value, const Range& validRange) +{ + size_t numberEnd = 0; + + if (numberEnd < value.size() && (value[numberEnd] == '+' || value[numberEnd] == '-')) + ++numberEnd; + while (numberEnd < value.size() && absl::ascii_isdigit(value[numberEnd])) + ++numberEnd; + + value = value.substr(0, numberEnd); + + int64_t returnedValue; + if (!absl::SimpleAtoi(value, &returnedValue)) + return absl::nullopt; + + if (returnedValue > std::numeric_limits::max()) + returnedValue = std::numeric_limits::max(); + if (returnedValue < std::numeric_limits::min()) + returnedValue = std::numeric_limits::min(); + + return validRange.clamp(static_cast(returnedValue)); +} + +template ::value, int>> +absl::optional readOpcode(absl::string_view value, const Range& validRange) +{ + size_t numberEnd = 0; + + if (numberEnd < value.size() && (value[numberEnd] == '+' || value[numberEnd] == '-')) + ++numberEnd; + while (numberEnd < value.size() && absl::ascii_isdigit(value[numberEnd])) + ++numberEnd; + + if (numberEnd < value.size() && value[numberEnd] == '.') { + ++numberEnd; + while (numberEnd < value.size() && absl::ascii_isdigit(value[numberEnd])) + ++numberEnd; + } + + value = value.substr(0, numberEnd); + + float returnedValue; + if (!absl::SimpleAtof(value, &returnedValue)) + return absl::nullopt; + + return validRange.clamp(returnedValue); +} + +absl::optional readBooleanFromOpcode(const Opcode& opcode) +{ + switch (hash(opcode.value)) { + case hash("off"): + return false; + case hash("on"): + return true; + default: + return {}; + } +} + +template +void setValueFromOpcode(const Opcode& opcode, ValueType& target, const Range& validRange) +{ + auto value = readOpcode(opcode.value, validRange); + if (!value) // Try and read a note rather than a number + value = readNoteValue(opcode.value); + if (value) + target = *value; +} + +template +inline void setValueFromOpcode(const Opcode& opcode, absl::optional& target, const Range& validRange) +{ + auto value = readOpcode(opcode.value, validRange); + if (!value) // Try and read a note rather than a number + value = readNoteValue(opcode.value); + if (value) + target = *value; +} + +template +void setRangeEndFromOpcode(const Opcode& opcode, Range& target, const Range& validRange) +{ + auto value = readOpcode(opcode.value, validRange); + if (!value) // Try and read a note rather than a number + value = readNoteValue(opcode.value); + if (value) + target.setEnd(*value); +} + +template +void setRangeStartFromOpcode(const Opcode& opcode, Range& target, const Range& validRange) +{ + auto value = readOpcode(opcode.value, validRange); + if (!value) // Try and read a note rather than a number + value = readNoteValue(opcode.value); + if (value) + target.setStart(*value); +} + +template +void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange) +{ + auto value = readOpcode(opcode.value, validRange); + if (value && Default::ccNumberRange.containsWithEnd(opcode.parameters.back())) + target = { opcode.parameters.back(), *value }; + else + target = {}; +} + +/// +#define INSTANCIATE_FOR(T) \ + template absl::optional readOpcode(absl::string_view value, const Range& validRange); \ + template void setValueFromOpcode(const Opcode& opcode, T& target, const Range& validRange); \ + template void setValueFromOpcode(const Opcode& opcode, absl::optional& target, const Range& validRange); \ + template void setRangeEndFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); \ + template void setRangeStartFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); \ + template void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange); + +INSTANCIATE_FOR(float) +INSTANCIATE_FOR(double) +INSTANCIATE_FOR(int8_t) +INSTANCIATE_FOR(int16_t) +INSTANCIATE_FOR(int32_t) +INSTANCIATE_FOR(int64_t) +INSTANCIATE_FOR(uint8_t) +INSTANCIATE_FOR(uint16_t) +INSTANCIATE_FOR(uint32_t) +//INSTANCIATE_FOR(uint64_t) + +#undef INSTANCIATE_FOR + +} // namespace sfz + std::ostream &operator<<(std::ostream &os, const sfz::Opcode &opcode) { return os << opcode.opcode << '=' << '"' << opcode.value << '"'; diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index 4d15f844..a3132499 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -125,28 +125,7 @@ absl::optional readNoteValue(absl::string_view value); * @return absl::optional the cast value, or null */ template ::value, int> = 0> -inline absl::optional readOpcode(absl::string_view value, const Range& validRange) -{ - size_t numberEnd = 0; - - if (numberEnd < value.size() && (value[numberEnd] == '+' || value[numberEnd] == '-')) - ++numberEnd; - while (numberEnd < value.size() && absl::ascii_isdigit(value[numberEnd])) - ++numberEnd; - - value = value.substr(0, numberEnd); - - int64_t returnedValue; - if (!absl::SimpleAtoi(value, &returnedValue)) - return absl::nullopt; - - if (returnedValue > std::numeric_limits::max()) - returnedValue = std::numeric_limits::max(); - if (returnedValue < std::numeric_limits::min()) - returnedValue = std::numeric_limits::min(); - - return validRange.clamp(static_cast(returnedValue)); -} +absl::optional readOpcode(absl::string_view value, const Range& validRange); /** * @brief Read a value from the sfz file and cast it to the destination parameter along @@ -159,44 +138,12 @@ inline absl::optional readOpcode(absl::string_view value, const Range * @return absl::optional the cast value, or null */ template ::value, int> = 0> -inline absl::optional readOpcode(absl::string_view value, const Range& validRange) -{ - size_t numberEnd = 0; - - if (numberEnd < value.size() && (value[numberEnd] == '+' || value[numberEnd] == '-')) - ++numberEnd; - while (numberEnd < value.size() && absl::ascii_isdigit(value[numberEnd])) - ++numberEnd; - - if (numberEnd < value.size() && value[numberEnd] == '.') { - ++numberEnd; - while (numberEnd < value.size() && absl::ascii_isdigit(value[numberEnd])) - ++numberEnd; - } - - value = value.substr(0, numberEnd); - - float returnedValue; - if (!absl::SimpleAtof(value, &returnedValue)) - return absl::nullopt; - - return validRange.clamp(returnedValue); -} +absl::optional readOpcode(absl::string_view value, const Range& validRange); /** * @brief Read a boolean value from the sfz file and cast it to the destination parameter. */ -inline absl::optional readBooleanFromOpcode(const Opcode& opcode) -{ - switch (hash(opcode.value)) { - case hash("off"): - return false; - case hash("on"): - return true; - default: - return {}; - } -} +absl::optional readBooleanFromOpcode(const Opcode& opcode); /** * @brief Set a target parameter from an opcode value, with possibly a textual note rather @@ -208,14 +155,7 @@ inline absl::optional readBooleanFromOpcode(const Opcode& opcode) * @param validRange the range of admitted values used to clamp the opcode */ template -inline void setValueFromOpcode(const Opcode& opcode, ValueType& target, const Range& validRange) -{ - auto value = readOpcode(opcode.value, validRange); - if (!value) // Try and read a note rather than a number - value = readNoteValue(opcode.value); - if (value) - target = *value; -} +void setValueFromOpcode(const Opcode& opcode, ValueType& target, const Range& validRange); /** * @brief Set a target parameter from an opcode value, with possibly a textual note rather @@ -227,14 +167,7 @@ inline void setValueFromOpcode(const Opcode& opcode, ValueType& target, const Ra * @param validRange the range of admitted values used to clamp the opcode */ template -inline void setValueFromOpcode(const Opcode& opcode, absl::optional& target, const Range& validRange) -{ - auto value = readOpcode(opcode.value, validRange); - if (!value) // Try and read a note rather than a number - value = readNoteValue(opcode.value); - if (value) - target = *value; -} +void setValueFromOpcode(const Opcode& opcode, absl::optional& target, const Range& validRange); /** * @brief Set a target end of a range from an opcode value, with possibly a textual note rather @@ -246,14 +179,7 @@ inline void setValueFromOpcode(const Opcode& opcode, absl::optional& * @param validRange the range of admitted values used to clamp the opcode */ template -inline void setRangeEndFromOpcode(const Opcode& opcode, Range& target, const Range& validRange) -{ - auto value = readOpcode(opcode.value, validRange); - if (!value) // Try and read a note rather than a number - value = readNoteValue(opcode.value); - if (value) - target.setEnd(*value); -} +void setRangeEndFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); /** * @brief Set a target beginning of a range from an opcode value, with possibly a textual note rather @@ -265,14 +191,7 @@ inline void setRangeEndFromOpcode(const Opcode& opcode, Range& target * @param validRange the range of admitted values used to clamp the opcode */ template -inline void setRangeStartFromOpcode(const Opcode& opcode, Range& target, const Range& validRange) -{ - auto value = readOpcode(opcode.value, validRange); - if (!value) // Try and read a note rather than a number - value = readNoteValue(opcode.value); - if (value) - target.setStart(*value); -} +void setRangeStartFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); /** * @brief Set a CC modulation parameter from an opcode value. @@ -283,14 +202,7 @@ inline void setRangeStartFromOpcode(const Opcode& opcode, Range& targ * @param validRange the range of admitted values used to clamp the opcode */ template -inline void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange) -{ - auto value = readOpcode(opcode.value, validRange); - if (value && Default::ccNumberRange.containsWithEnd(opcode.parameters.back())) - target = { opcode.parameters.back(), *value }; - else - target = {}; -} +void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange); } From 0d6195a42e1e5ce0d59ad41333bd5511a885ff57 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 13 Aug 2020 18:51:31 +0200 Subject: [PATCH 112/445] No lint for bugprone-macro-parentheses --- src/sfizz/Opcode.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Opcode.cpp b/src/sfizz/Opcode.cpp index 6ec196a2..1ffafdbf 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -272,12 +272,12 @@ void setCCPairFromOpcode(const Opcode& opcode, absl::optional> /// #define INSTANCIATE_FOR(T) \ - template absl::optional readOpcode(absl::string_view value, const Range& validRange); \ - template void setValueFromOpcode(const Opcode& opcode, T& target, const Range& validRange); \ - template void setValueFromOpcode(const Opcode& opcode, absl::optional& target, const Range& validRange); \ - template void setRangeEndFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); \ - template void setRangeStartFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); \ - template void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange); + template absl::optional readOpcode(absl::string_view value, const Range& validRange); /*NOLINT(bugprone-macro-parentheses)*/ \ + template void setValueFromOpcode(const Opcode& opcode, T& target, const Range& validRange); /*NOLINT(bugprone-macro-parentheses)*/ \ + template void setValueFromOpcode(const Opcode& opcode, absl::optional& target, const Range& validRange); /*NOLINT(bugprone-macro-parentheses)*/ \ + template void setRangeEndFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); /*NOLINT(bugprone-macro-parentheses)*/ \ + template void setRangeStartFromOpcode(const Opcode& opcode, Range& target, const Range& validRange); /*NOLINT(bugprone-macro-parentheses)*/ \ + template void setCCPairFromOpcode(const Opcode& opcode, absl::optional>& target, const Range& validRange); /*NOLINT(bugprone-macro-parentheses)*/ INSTANCIATE_FOR(float) INSTANCIATE_FOR(double) From ad2dda35950bf2307f4ae7b92788bf810d782f4f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 13 Aug 2020 22:48:05 +0200 Subject: [PATCH 113/445] Round audio file detune to the exact cent --- src/sfizz/FileMetadata.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FileMetadata.cpp b/src/sfizz/FileMetadata.cpp index e57f70d5..dff53ac3 100644 --- a/src/sfizz/FileMetadata.cpp +++ b/src/sfizz/FileMetadata.cpp @@ -263,7 +263,7 @@ bool FileMetadataReader::extractRiffInstrument(SF_INSTRUMENT& ins) ins.gain = 1; ins.basenote = extractU32(0x14 - 8); ins.detune = static_cast( // Q0,32 semitones to cents - (static_cast(extractU32(0x18 - 8)) * 100) >> 32); + std::lround(extractU32(0x18 - 8) * (100.0 / (static_cast(1) << 32)))); ins.velocity_lo = 0; ins.velocity_hi = 127; ins.key_lo = 0; From aba8d966ab5ae74dc51e56c14975a37cfc90f229 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 13 Aug 2020 22:50:20 +0200 Subject: [PATCH 114/445] Include in FileMetadata --- src/sfizz/FileMetadata.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/FileMetadata.cpp b/src/sfizz/FileMetadata.cpp index dff53ac3..60f6d64e 100644 --- a/src/sfizz/FileMetadata.cpp +++ b/src/sfizz/FileMetadata.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace sfz { From 57c8b44cfaa4a466eae939e9ec8884ba88141a30 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 14 Aug 2020 17:53:43 +0200 Subject: [PATCH 115/445] Support type atom:Blank for older LV2 hosts --- lv2/sfizz.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index ef35867c..ef07343e 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -130,6 +130,7 @@ typedef struct LV2_URID nominal_block_length_uri; LV2_URID sample_rate_uri; LV2_URID atom_object_uri; + LV2_URID atom_blank_uri; LV2_URID atom_float_uri; LV2_URID atom_double_uri; LV2_URID atom_int_uri; @@ -240,6 +241,7 @@ sfizz_lv2_map_required_uris(sfizz_plugin_t *self) self->atom_path_uri = map->map(map->handle, LV2_ATOM__Path); self->atom_urid_uri = map->map(map->handle, LV2_ATOM__URID); self->atom_object_uri = map->map(map->handle, LV2_ATOM__Object); + self->atom_blank_uri = map->map(map->handle, LV2_ATOM__Blank); self->patch_set_uri = map->map(map->handle, LV2_PATCH__Set); self->patch_get_uri = map->map(map->handle, LV2_PATCH__Get); self->patch_put_uri = map->map(map->handle, LV2_PATCH__Put); @@ -865,7 +867,7 @@ run(LV2_Handle instance, uint32_t sample_count) const int delay = (int)ev->time.frames; // If the received atom is an object/patch message - if (ev->body.type == self->atom_object_uri) + if (ev->body.type == self->atom_object_uri || ev->body.type == self->atom_blank_uri) { const LV2_Atom_Object *obj = (const LV2_Atom_Object *)&ev->body; if (obj->body.otype == self->patch_set_uri) From f5d61fe1837ea14bd79e8c0b02307e3f7101634a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 17 Aug 2020 15:31:58 +0200 Subject: [PATCH 116/445] Fix amp_veltrack --- src/sfizz/Region.cpp | 18 ++++-------------- src/sfizz/Region.h | 2 +- src/sfizz/Synth.cpp | 5 ++++- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index c3649054..fb6ba39e 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -432,7 +432,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, ampKeytrack, Default::ampKeytrackRange); break; case hash("amp_veltrack"): - setValueFromOpcode(opcode, ampVeltrack, Default::ampVeltrackRange); + if (auto value = readOpcode(opcode.value, Default::ampVeltrackRange)) + ampVeltrack = normalizePercents(*value); break; case hash("amp_random"): setValueFromOpcode(opcode, ampRandom, Default::ampRandomRange); @@ -1517,19 +1518,8 @@ float sfz::Region::velocityCurve(float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - float gain { 1.0f }; - if (velCurve) { // Custom velocity curve - return velCurve->evalNormalized(velocity); - } else { // Standard velocity curve - // FIXME: Maybe there's a prettier way to check the boundaries? - const float gaindB = [&]() { - if (ampVeltrack >= 0) - return velocity == 0.0f ? -90.0f : 40 * std::log(velocity) / std::log(10.0f); - else - return velocity == 1.0f ? -90.0f : 40 * std::log(1 - velocity) / std::log(10.0f); - }(); - gain *= db2mag( gaindB * std::abs(ampVeltrack) / sfz::Default::ampVeltrackRange.getEnd()); - } + float gain = std::fabs(ampVeltrack) * (1.0f - velCurve->evalNormalized(velocity)); + gain = (ampVeltrack < 0) ? gain : (1.0f - gain); return gain; } diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 4bc1a91f..4935a85e 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -331,7 +331,7 @@ struct Region { float position { normalizePercents(Default::position) }; // position uint8_t ampKeycenter { Default::ampKeycenter }; // amp_keycenter float ampKeytrack { Default::ampKeytrack }; // amp_keytrack - float ampVeltrack { Default::ampVeltrack }; // amp_keytrack + float ampVeltrack { normalizePercents(Default::ampVeltrack) }; // amp_keytrack std::vector> velocityPoints; // amp_velcurve_N absl::optional velCurve {}; float ampRandom { Default::ampRandom }; // amp_random diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 3c15addf..3307a8e9 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -568,7 +568,10 @@ void sfz::Synth::finalizeSfzLoad() if (!region->velocityPoints.empty()) region->velCurve = Curve::buildFromVelcurvePoints( - region->velocityPoints, Curve::Interpolator::Linear, region->ampVeltrack < 0.0f); + region->velocityPoints, Curve::Interpolator::Linear); + else + region->velCurve = resources.curves.getCurve(4); + region->registerPitchWheel(0); region->registerAftertouch(0); region->registerTempo(2.0f); From 06d111cbd4b49427f10eb34dff0c3e187655f401 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 17 Aug 2020 16:01:58 +0200 Subject: [PATCH 117/445] Do not require a curve for the concave case (test helper) --- src/sfizz/Region.cpp | 8 +++++++- src/sfizz/Synth.cpp | 2 -- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index fb6ba39e..2d96835a 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1518,7 +1518,13 @@ float sfz::Region::velocityCurve(float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - float gain = std::fabs(ampVeltrack) * (1.0f - velCurve->evalNormalized(velocity)); + float gain; + if (velCurve) + gain = velCurve->evalNormalized(velocity); + else + gain = velocity * velocity; + + gain = std::fabs(ampVeltrack) * (1.0f - gain); gain = (ampVeltrack < 0) ? gain : (1.0f - gain); return gain; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 3307a8e9..9f095c4c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -569,8 +569,6 @@ void sfz::Synth::finalizeSfzLoad() if (!region->velocityPoints.empty()) region->velCurve = Curve::buildFromVelcurvePoints( region->velocityPoints, Curve::Interpolator::Linear); - else - region->velCurve = resources.curves.getCurve(4); region->registerPitchWheel(0); region->registerAftertouch(0); From be252361b6da9293cae34109bd1730a1f9eaca27 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 17 Aug 2020 16:10:08 +0200 Subject: [PATCH 118/445] Update existing tests and add new ones --- tests/RegionT.cpp | 10 ++--- tests/SynthT.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 051a4fe5..9ea9b924 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -692,15 +692,15 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("amp_veltrack") { - REQUIRE(region.ampVeltrack == 100.0f); + REQUIRE(region.ampVeltrack == 1.0f); region.parseOpcode({ "amp_veltrack", "4.2" }); - REQUIRE(region.ampVeltrack == 4.2f); + REQUIRE(region.ampVeltrack == Approx(0.042f)); region.parseOpcode({ "amp_veltrack", "-4.2" }); - REQUIRE(region.ampVeltrack == -4.2f); + REQUIRE(region.ampVeltrack == Approx(-0.042f)); region.parseOpcode({ "amp_veltrack", "-123" }); - REQUIRE(region.ampVeltrack == -100.0f); + REQUIRE(region.ampVeltrack == -1.0f); region.parseOpcode({ "amp_veltrack", "132" }); - REQUIRE(region.ampVeltrack == 100.0f); + REQUIRE(region.ampVeltrack == 1.0f); } SECTION("amp_random") diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 7795db93..8f0e94a5 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -448,16 +448,89 @@ TEST_CASE("[Synth] velcurve") amp_velcurve_064=1 sample=*sine amp_velcurve_064=1 amp_veltrack=-100 sample=*sine )"); - REQUIRE( synth.getRegionView(0)->velocityCurve(0_norm) == 0.0_a ); - REQUIRE( synth.getRegionView(0)->velocityCurve(32_norm) == Approx(0.5f).margin(1e-2) ); - REQUIRE( synth.getRegionView(0)->velocityCurve(64_norm) == 1.0_a ); - REQUIRE( synth.getRegionView(0)->velocityCurve(96_norm) == 1.0_a ); - REQUIRE( synth.getRegionView(0)->velocityCurve(127_norm) == 1.0_a ); - REQUIRE( synth.getRegionView(1)->velocityCurve(0_norm) == 1.0_a ); - REQUIRE( synth.getRegionView(1)->velocityCurve(32_norm) == 1.0_a ); - REQUIRE( synth.getRegionView(1)->velocityCurve(64_norm) == 1.0_a ); - REQUIRE( synth.getRegionView(1)->velocityCurve(96_norm) == Approx(0.5f).margin(1e-2) ); - REQUIRE( synth.getRegionView(1)->velocityCurve(127_norm) == 0.0_a ); + + struct VelocityData { float velocity, gain; bool exact; }; + + static const VelocityData veldata[] = { + { 0_norm, 0.0, true }, + { 32_norm, 0.5f, false }, + { 64_norm, 1.0, true }, + { 96_norm, 1.0, true }, + { 127_norm, 1.0, true }, + }; + + REQUIRE(synth.getNumRegions() == 2); + const sfz::Region* r1 = synth.getRegionView(0); + const sfz::Region* r2 = synth.getRegionView(1); + + for (const VelocityData& vd : veldata) { + if (vd.exact) { + REQUIRE(r1->velocityCurve(vd.velocity) == vd.gain); + REQUIRE(r2->velocityCurve(vd.velocity) == 1.0f - vd.gain); + } + else { + REQUIRE(r1->velocityCurve(vd.velocity) == Approx(vd.gain).margin(1e-2)); + REQUIRE(r2->velocityCurve(vd.velocity) == Approx(1.0f - vd.gain).margin(1e-2)); + } + } +} + +TEST_CASE("[Synth] veltrack") +{ + struct VelocityData { float velocity, dBGain; }; + struct VeltrackData { float veltrack; absl::Span veldata; }; + + // measured on ARIA + const VelocityData veldata25[] = { + { 127_norm, 0.0 }, + { 96_norm, -1 }, + { 64_norm, -1.8 }, + { 32_norm, -2.3 }, + { 1_norm, -2.5 }, + }; + const VelocityData veldata50[] = { + { 127_norm, 0.0 }, + { 96_norm, -2.1 }, + { 64_norm, -4.1 }, + { 32_norm, -5.5 }, + { 1_norm, -6.0 }, + }; + const VelocityData veldata75[] = { + { 127_norm, 0.0 }, + { 96_norm, -3.4 }, + { 64_norm, -7.2 }, + { 32_norm, -10.5 }, + { 1_norm, -12.0 }, + }; + const VelocityData veldata100[] = { + { 127_norm, 0.0 }, + { 96_norm, -4.9 }, + { 64_norm, -12.0 }, + { 32_norm, -24.0 }, + { 1_norm, -84.1 }, + }; + + const VeltrackData veltrackdata[] = { + { 25, veldata25 }, + { 50, veldata50 }, + { 75, veldata75 }, + { 100, veldata100 }, + }; + + for (const VeltrackData& vt : veltrackdata) { + sfz::Synth synth; + const std::string sfzCode = "sample=*sine amp_veltrack=" + + std::to_string(vt.veltrack); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/veltrack.sfz", sfzCode); + + REQUIRE(synth.getNumRegions() == 1); + const sfz::Region* r = synth.getRegionView(0); + + for (const VelocityData& vd : vt.veldata) { + float dBGain = 20.0f * std::log10(r->velocityCurve(vd.velocity)); + REQUIRE(dBGain == Approx(vd.dBGain).margin(0.1)); + } + } } TEST_CASE("[Synth] Region by identifier") From 4c0a7f6dfd6fa1b922e7788912ad3b315ef2e93c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 18 Aug 2020 18:15:20 +0200 Subject: [PATCH 119/445] Adjust the ADSR exponential rate --- src/sfizz/ADSREnvelope.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index bfd7f3f8..472c9fd7 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -28,7 +28,7 @@ template Type ADSREnvelope::secondsToExpRate (Type timeInSeconds) const noexcept { timeInSeconds = std::max(25e-3, timeInSeconds); - return std::exp(-8.0 / (timeInSeconds * sampleRate)); + return std::exp(-9.0 / (timeInSeconds * sampleRate)); }; template From 12e2c95f5ce161dd868c4d5463273e922cad53b1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 18 Aug 2020 19:43:36 +0200 Subject: [PATCH 120/445] Set the threshold of ADSR release to -80 dB --- src/sfizz/ADSREnvelope.cpp | 6 +++--- src/sfizz/Config.h | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index 472c9fd7..b17bce09 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -96,7 +96,7 @@ Type ADSREnvelope::getNextValue() noexcept return currentValue; case State::Release: currentValue *= releaseRate; - if (currentValue > config::virtuallyZero) + if (currentValue > config::egReleaseThreshold) return currentValue; currentState = State::Done; @@ -169,9 +169,9 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept sfz::fill(output.first(count), currentValue); break; case State::Release: - while (count < size && (currentValue *= releaseRate) > config::virtuallyZero) + while (count < size && (currentValue *= releaseRate) > config::egReleaseThreshold) output[count++] = currentValue; - if (currentValue <= config::virtuallyZero) { + if (currentValue <= config::egReleaseThreshold) { currentValue = 0; currentState = State::Done; } diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 3ec5f1a8..c13707ef 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -86,6 +86,11 @@ namespace config { modulated filter. The lower, the more CPU resources are consumed. */ constexpr int filterControlInterval { 16 }; + /** + Amplitude below which an exponential releasing envelope is considered as + finished. + */ + constexpr float egReleaseThreshold = 1e-4; /** Default metadata for MIDIName documents */ From 05de17e32ee335c951665d5171b7052ea6da8373 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 18 Aug 2020 22:36:00 +0200 Subject: [PATCH 121/445] The sustain cc is still a CC and has to be registered in the regions --- src/sfizz/Synth.cpp | 8 ++++---- tests/SynthT.cpp | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 9f095c4c..f2eb3f0b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1062,10 +1062,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept return matchReleaseRegionAndVoice(*region, *v); }; - if (absl::c_find_if(voices, compatibleVoice) == voices.end()) { + if (absl::c_find_if(voices, compatibleVoice) == voices.end()) region->delayedReleases.clear(); - continue; - } } for (auto& note: region->delayedReleases) { @@ -1082,7 +1080,9 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept } region->delayedReleases.clear(); - } else if (region->registerCC(ccNumber, normValue)) { + } + + if (region->registerCC(ccNumber, normValue)) { auto voice = findFreeVoice(); if (voice == nullptr) continue; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 8f0e94a5..cf933ead 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1120,3 +1120,26 @@ TEST_CASE("[Synth] Used CCs EGs") // REQUIRE( usedCCs[27] ); // REQUIRE( !usedCCs[28] ); } + +TEST_CASE("[Synth] Activate also on the sustain CC") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + locc64=64 key=53 sample=*sine + )"); + synth.noteOn(0, 53, 127); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.cc(1, 64, 127); + synth.noteOn(2, 53, 127); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); +} + +TEST_CASE("[Synth] Trigger also on the sustain CC") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + on_locc64=64 sample=*sine + )"); + synth.cc(0, 64, 127); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); +} From da75095a9b18a78355460a6bd361dc3b46333714 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 18 Aug 2020 23:09:03 +0200 Subject: [PATCH 122/445] Explicitely signify if a loop if present Before we tried to "guess", which causes problems if a loop spans the entire file --- src/sfizz/FilePool.cpp | 1 + src/sfizz/FilePool.h | 1 + src/sfizz/Synth.cpp | 2 +- tests/FilesT.cpp | 11 +++++++++++ .../wavetable_with_loop_at_endings.wav | Bin 0 -> 33004 bytes 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/TestFiles/wavetable_with_loop_at_endings.wav diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index cda13a7e..6e27153b 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -227,6 +227,7 @@ absl::optional sfz::FilePool::getFileInformation(const Fil if (!fileId.isReverse()) { if (instrumentInfo.loop_count > 0) { + returnedValue.hasLoop = true; returnedValue.loopBegin = instrumentInfo.loops[0].start; returnedValue.loopEnd = min(returnedValue.end, instrumentInfo.loops[0].end - 1); } diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index cbec5f8f..9d099550 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -52,6 +52,7 @@ struct FileInformation { uint32_t end { Default::sampleEndRange.getEnd() }; uint32_t loopBegin { Default::loopRange.getStart() }; uint32_t loopEnd { Default::loopRange.getEnd() }; + bool hasLoop { false }; double sampleRate { config::defaultSampleRate }; int numChannels { 0 }; }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 9f095c4c..40dd331b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -482,7 +482,7 @@ void sfz::Synth::finalizeSfzLoad() region->sampleEnd = std::min(region->sampleEnd, fileInformation->end); - if (fileInformation->loopBegin != Default::loopRange.getStart() && fileInformation->loopEnd != Default::loopRange.getEnd()) { + if (fileInformation->hasLoop) { if (region->loopRange.getStart() == Default::loopRange.getStart()) region->loopRange.setStart(fileInformation->loopBegin); diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 98b16dce..f92f496c 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -564,6 +564,17 @@ TEST_CASE("[Files] Looped regions taken from files and possibly overriden") REQUIRE(synth.getRegionView(2)->loopRange == Range { 4, 124 }); } +TEST_CASE("[Files] Looped regions can start at 0") +{ + Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/loop_can_start_at_0.sfz", R"( + sample=wavetable_with_loop_at_endings.wav + )"); + REQUIRE( synth.getNumRegions() == 1 ); + REQUIRE( synth.getRegionView(0)->loopMode == SfzLoopMode::loop_continuous ); + REQUIRE( synth.getRegionView(0)->loopRange == Range { 0, synth.getRegionView(0)->sampleEnd } ); +} + TEST_CASE("[Files] Case sentitiveness") { const fs::path sfzFilePath = fs::current_path() / "tests/TestFiles/case_insensitive.sfz"; diff --git a/tests/TestFiles/wavetable_with_loop_at_endings.wav b/tests/TestFiles/wavetable_with_loop_at_endings.wav new file mode 100644 index 0000000000000000000000000000000000000000..b6aafda4930b4b4e696637a71d6ef2732a3009d9 GIT binary patch literal 33004 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 Date: Wed, 19 Aug 2020 06:15:12 +0200 Subject: [PATCH 123/445] Match the volumes to ARIA, add CC7 and CC10 --- src/sfizz/Config.h | 12 ++++++++---- src/sfizz/Synth.cpp | 14 ++++++++++++++ src/sfizz/Voice.cpp | 4 ++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index c13707ef..2956dae6 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -103,10 +103,14 @@ namespace config { // Wavetable constants; amplitude values are matched to reference static constexpr unsigned tableSize = 1024; static constexpr double tableRefSampleRate = 44100.0 * 1.1; // +10% aliasing permissivity - static constexpr double amplitudeSine = 0.625; - static constexpr double amplitudeTriangle = 0.625; - static constexpr double amplitudeSaw = 0.515; - static constexpr double amplitudeSquare = 0.515; + /** + Default wave amplitudes, adjusted for consistent RMS among all waves. + (except square curiously, but it's to match ARIA) + */ + static constexpr double amplitudeSine = 1.0; + static constexpr double amplitudeTriangle = 1.0; + static constexpr double amplitudeSaw = 0.8164965809277261; // sqrt(2)/sqrt(3) + static constexpr double amplitudeSquare = 0.8164965809277261; // should have been sqrt(2)? /** Background file loading */ diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index dcbffc6f..0ce7d527 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -143,6 +143,16 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) int regionNumber = static_cast(regions.size()); auto lastRegion = absl::make_unique(regionNumber, resources.midiState, defaultPath); + // Create default connections + constexpr unsigned defaultSmoothness = 10; + lastRegion->getOrCreateConnection( + ModKey::createCC(7, 4, defaultSmoothness, 100, 0), + ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)); + lastRegion->getOrCreateConnection( + ModKey::createCC(10, 1, defaultSmoothness, 100, 0), + ModKey::createNXYZ(ModId::Pan, lastRegion->id)); + + // auto parseOpcodes = [&](const std::vector& opcodes) { for (auto& opcode : opcodes) { const auto unknown = absl::c_find_if(unknownOpcodes, [&](absl::string_view sv) { return sv.compare(opcode.opcode) == 0; }); @@ -215,6 +225,10 @@ void sfz::Synth::clear() polyphonyGroups.emplace_back(); polyphonyGroups.back().setPolyphonyLimit(config::maxVoices); modificationTime = fs::file_time_type::min(); + + // set default controllers + cc(0, 7, 100); // volume + hdcc(0, 10, 0.5f); // pan } void sfz::Synth::handleMasterOpcodes(const std::vector& members) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 331ae748..d7517a3d 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -491,6 +491,10 @@ void sfz::Voice::panStageStereo(AudioSpan buffer) noexcept (*modulationSpan)[i] += normalizePercents(mod[i]); } pan(*modulationSpan, leftBuffer, rightBuffer); + + // add +3dB to compensate for the 2 pan stages (-3dB each stage) + applyGain1(1.4125375446227544f, leftBuffer); + applyGain1(1.4125375446227544f, rightBuffer); } void sfz::Voice::filterStageMono(AudioSpan buffer) noexcept From e5ec42f21b78ec1d3d8161456f39c3f638d34ad1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 19 Aug 2020 06:31:41 +0200 Subject: [PATCH 124/445] Fix a problem in SIMD helper declarations --- src/sfizz/SIMDHelpers.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/SIMDHelpers.h b/src/sfizz/SIMDHelpers.h index 1cfa48d1..c0a27c09 100644 --- a/src/sfizz/SIMDHelpers.h +++ b/src/sfizz/SIMDHelpers.h @@ -176,13 +176,13 @@ inline void applyGain1(T gain, absl::Span input, absl::Span output) * @param size */ template -inline void applyGain1(float gain, float* array, unsigned size) noexcept +inline void applyGain1(T gain, T* array, unsigned size) noexcept { applyGain1(gain, array, array, size); } template -inline void applyGain1(float gain, absl::Span array) noexcept +inline void applyGain1(T gain, absl::Span array) noexcept { applyGain1(gain, array.data(), array.data(), array.size()); } From f217c90619772fa4a80692e004336d749b200d3f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 19 Aug 2020 07:07:37 +0200 Subject: [PATCH 125/445] Update tests --- tests/ModulationsT.cpp | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 709ec36d..48203a2e 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -78,6 +78,32 @@ TEST_CASE("[Modulations] Display names") }); } +static std::string createReferenceGraph(std::vector lines) +{ + const char* defaultConnections[] = { + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude")", + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan")" + }; + + for (const char* line : defaultConnections) + lines.push_back(line); + + std::sort(lines.begin(), lines.end()); + + std::string graph; + graph.reserve(1024); + + graph += "digraph {\n"; + for (const std::string& line : lines) { + graph.push_back('\t'); + graph += line; + graph.push_back('\n'); + } + graph += "}\n"; + + return graph; +}; + TEST_CASE("[Modulations] Connection graph from SFZ") { sfz::Synth synth; @@ -89,12 +115,12 @@ pitch_oncc42=71 pitch_smoothcc42=32 pan_oncc36=14.5 pan_stepcc36=1.5 width_oncc425=29 )"); + const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == R"(digraph { - "Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude" - "Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan" - "Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch" - "Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width" -} -)"); + REQUIRE(graph == createReferenceGraph({ + R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude")", + R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch")", + R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan")", + R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width")", + })); } From 80aee5a9cdaf37a895d811301af552769f27f543 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 00:47:31 +0200 Subject: [PATCH 126/445] Add a helper for playing voices --- tests/TestHelpers.cpp | 11 +++++++++++ tests/TestHelpers.h | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index ca32de0b..f58eee73 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -51,6 +51,17 @@ const std::vector getActiveVoices(const sfz::Synth& synth) return activeVoices; } +const std::vector getPlayingVoices(const sfz::Synth& synth) +{ + std::vector playingVoices; + for (int i = 0; i < synth.getNumVoices(); ++i) { + const auto* voice = synth.getVoiceView(i); + if (!voice->releasedOrFree()) + playingVoices.push_back(voice); + } + return playingVoices; +} + unsigned numPlayingVoices(const sfz::Synth& synth) { return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) { diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index ac8be3dc..e5e48671 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -49,6 +49,15 @@ void sortAll(C& container, Args&... others) */ const std::vector getActiveVoices(const sfz::Synth& synth); +/** + * @brief Get playing (unreleased) voices from the synth + * + * @param synth + * @return const std::vector + */ +const std::vector getPlayingVoices(const sfz::Synth& synth); + + /** * @brief Count the number of playing (unreleased) voices from the synth * From 5c61298f08f6eed9a1f754bd3bf2f3734e09e828 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 00:48:20 +0200 Subject: [PATCH 127/445] A region is disabled by end=-1 --- src/sfizz/Region.cpp | 6 +++++- src/sfizz/Region.h | 1 + tests/RegionT.cpp | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 2d96835a..cad035b8 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -92,7 +92,11 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) offsetCC[opcode.parameters.back()] = *value; break; case hash("end"): - setValueFromOpcode(opcode, sampleEnd, Default::sampleEndRange); + if (opcode.value == "-1") { + disabled = true; + } else { + setValueFromOpcode(opcode, sampleEnd, Default::sampleEndRange); + } break; case hash("count"): setValueFromOpcode(opcode, sampleCount, Default::sampleCountRange); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 4935a85e..4dc5cdef 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -272,6 +272,7 @@ struct Region { int64_t offsetRandom { Default::offsetRandom }; // offset_random CCMap offsetCC { Default::offset }; uint32_t sampleEnd { Default::sampleEndRange.getEnd() }; // end + bool disabled { false }; // end=-1 and other disabling events absl::optional sampleCount {}; // count absl::optional loopMode {}; // loopmode Range loopRange { Default::loopRange }; //loopstart and loopend diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 9ea9b924..d4e68982 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -95,7 +95,7 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "end", "184" }); REQUIRE(region.sampleEnd == 184); region.parseOpcode({ "end", "-1" }); - REQUIRE(region.sampleEnd == 0); + REQUIRE(region.disabled); } SECTION("count") From cef44cbdb67aba8ef448c1736d9a455d88f01bf1 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 00:50:09 +0200 Subject: [PATCH 128/445] Disabled regions don't need to take up polyphony The tests were a bit flaky and the did not seem justified by any behavior in e.g. ARIA The new tests cover a similar situation which is validated in sforzando --- src/sfizz/Voice.cpp | 7 ++-- tests/FilesT.cpp | 50 ---------------------------- tests/SynthT.cpp | 80 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 53 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index d7517a3d..86dd1cd4 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -51,6 +51,10 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, triggerValue = value; this->region = region; + + if (region->disabled) + return; + switchState(State::playing); ASSERT(delay >= 0); @@ -715,9 +719,6 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept if (region == nullptr) return false; - if (delay <= this->triggerDelay) - return false; - if (triggerType == TriggerType::NoteOn && region->offBy == group) { off(delay); return true; diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index f92f496c..fdc78691 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -471,56 +471,6 @@ TEST_CASE("[Files] Note and octave offsets") REQUIRE( synth.getRegionView(6)->pitchKeycenter == 50 ); } -TEST_CASE("[Files] Off by with different delays") -{ - Synth synth; - synth.setSamplesPerBlock(256); - AudioBuffer buffer(2, 256); - synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz"); - REQUIRE( synth.getNumRegions() == 4 ); - synth.noteOn(0, 63, 63); - REQUIRE( synth.getNumActiveVoices(true) == 1 ); - auto group1Voice = synth.getVoiceView(0); - REQUIRE( group1Voice->getRegion()->group == 1ul ); - REQUIRE( group1Voice->getRegion()->offBy == 2ul ); - synth.noteOn(100, 64, 63); - synth.renderBlock(buffer); - REQUIRE(group1Voice->releasedOrFree()); -} - -TEST_CASE("[Files] Off by with the same delays") -{ - Synth synth; - synth.setSamplesPerBlock(256); - synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz"); - REQUIRE( synth.getNumRegions() == 4 ); - synth.noteOn(0, 63, 63); - REQUIRE( synth.getNumActiveVoices(true) == 1 ); - auto group1Voice = synth.getVoiceView(0); - REQUIRE( group1Voice->getRegion()->group == 1ul ); - REQUIRE( group1Voice->getRegion()->offBy == 2ul ); - synth.noteOn(0, 64, 63); - REQUIRE(!group1Voice->releasedOrFree()); -} - -TEST_CASE("[Files] Off by with the same notes at the same time") -{ - Synth synth; - synth.setSamplesPerBlock(256); - synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz"); - REQUIRE( synth.getNumRegions() == 4 ); - synth.noteOn(0, 65, 63); - REQUIRE( synth.getNumActiveVoices(true) == 2 ); - synth.noteOn(0, 65, 63); - REQUIRE( synth.getNumActiveVoices(true) == 4 ); - AudioBuffer buffer { 2, 256 }; - synth.renderBlock(buffer); - synth.noteOn(0, 65, 63); - synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices(true) == 6 ); - REQUIRE( numPlayingVoices(synth) == 2 ); -} - TEST_CASE("[Files] Off modes") { Synth synth; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index cf933ead..5d167385 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1143,3 +1143,83 @@ TEST_CASE("[Synth] Trigger also on the sustain CC") synth.cc(0, 64, 127); REQUIRE( synth.getNumActiveVoices(true) == 1 ); } + +TEST_CASE("[Synth] end=-1 voices are immediately killed after triggering but they kill other voices") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path(), R"( + key=60 end=-1 sample=*sine + key=61 end=-1 sample=*silence + key=62 sample=*sine off_by=2 + key=63 end=-1 sample=*saw group=2 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 61, 85); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(1, 63, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 0 ); +} + +TEST_CASE("[Synth] Off by standard") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path(), R"( + group=1 off_by=2 sample=*saw transpose=12 key=60 + group=2 off_by=1 sample=*triangle key=62 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(10, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + auto playingVoices = getPlayingVoices(synth); + REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(62) ); + synth.noteOn(10, 60, 85); + playingVoices = getPlayingVoices(synth); + REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(60) ); +} + +TEST_CASE("[Synth] Off by same group") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path(), R"( + group=1 off_by=1 sample=*saw transpose=12 key=60 + group=1 off_by=1 sample=*triangle key=62 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(10, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + auto playingVoices = getPlayingVoices(synth); + REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(62) ); + synth.noteOn(10, 60, 85); + playingVoices = getPlayingVoices(synth); + REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(60) ); +} + + +TEST_CASE("[Synth] Off by same note") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path(), R"( + group=1 off_by=1 sample=*saw transpose=12 key=60 + group=1 off_by=1 sample=*triangle key=60 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + auto playingVoices = getPlayingVoices(synth); + REQUIRE( playingVoices.front()->getRegion()->sampleId.filename() == "*triangle" ); +} + From 17568510ac91e1e15c965bdf681b3b802ca71d51 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 11:06:16 +0200 Subject: [PATCH 129/445] Disable with sampleEnd == 0 --- src/sfizz/Region.cpp | 16 ++++++++++------ src/sfizz/Region.h | 6 +++++- src/sfizz/Voice.cpp | 2 +- tests/RegionT.cpp | 7 ++++++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index cad035b8..8843ce5c 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -92,11 +92,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) offsetCC[opcode.parameters.back()] = *value; break; case hash("end"): - if (opcode.value == "-1") { - disabled = true; - } else { - setValueFromOpcode(opcode, sampleEnd, Default::sampleEndRange); - } + setValueFromOpcode(opcode, sampleEnd, Default::sampleEndRange); break; case hash("count"): setValueFromOpcode(opcode, sampleCount, Default::sampleCountRange); @@ -1462,7 +1458,10 @@ float sfz::Region::getDelay() const noexcept uint32_t sfz::Region::trueSampleEnd(Oversampling factor) const noexcept { - return min(sampleEnd, loopRange.getEnd()) * static_cast(factor); + if (sampleEnd <= 0) + return 0; + + return min(static_cast(sampleEnd), loopRange.getEnd()) * static_cast(factor); } uint32_t sfz::Region::loopStart(Oversampling factor) const noexcept @@ -1619,3 +1618,8 @@ sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source connections.push_back(c); return connections.back(); } + +bool sfz::Region::disabled() const noexcept +{ + return (sampleEnd == 0); +} diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 4dc5cdef..e5fbb6c0 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -261,6 +261,11 @@ struct Region { */ float getGainToEffectBus(unsigned number) const noexcept; + /** + * @brief Check if a region is disabled, if its sample end is weakly negative for example. + */ + bool disabled() const noexcept; + const NumericId id; // Sound source: sample playback @@ -272,7 +277,6 @@ struct Region { int64_t offsetRandom { Default::offsetRandom }; // offset_random CCMap offsetCC { Default::offset }; uint32_t sampleEnd { Default::sampleEndRange.getEnd() }; // end - bool disabled { false }; // end=-1 and other disabling events absl::optional sampleCount {}; // count absl::optional loopMode {}; // loopmode Range loopRange { Default::loopRange }; //loopstart and loopend diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 86dd1cd4..3321d761 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -52,7 +52,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, this->region = region; - if (region->disabled) + if (region->disabled()) return; switchState(State::playing); diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index d4e68982..fa3f8f4c 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -95,7 +95,12 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "end", "184" }); REQUIRE(region.sampleEnd == 184); region.parseOpcode({ "end", "-1" }); - REQUIRE(region.disabled); + REQUIRE(region.disabled()); + region.parseOpcode({ "end", "2" }); + REQUIRE(!region.disabled()); + REQUIRE(region.sampleEnd == 2); + region.parseOpcode({ "end", "0" }); + REQUIRE(region.disabled()); } SECTION("count") From fae0cda97139a162194d32923d68ea01006240f7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 11:06:26 +0200 Subject: [PATCH 130/445] Correct a compilation woe --- tests/DataHelpers.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/DataHelpers.cpp b/tests/DataHelpers.cpp index 1f0bd949..a5cb7df0 100644 --- a/tests/DataHelpers.cpp +++ b/tests/DataHelpers.cpp @@ -10,6 +10,7 @@ #include #include #include +#include void load_txt(DataPoints& dp, std::istream& in) { From 1cf5e8d571a932b77020f778732a273acc9e160b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 18 Aug 2020 23:13:11 +0200 Subject: [PATCH 131/445] Set loop_mode to one_shot for release regions --- src/sfizz/Region.cpp | 2 ++ tests/RegionT.cpp | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 2d96835a..b940031a 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -349,9 +349,11 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) break; case hash("release"): trigger = SfzTrigger::release; + loopMode = SfzLoopMode::one_shot; break; case hash("release_key"): trigger = SfzTrigger::release_key; + loopMode = SfzLoopMode::one_shot; break; default: DBG("Unknown trigger mode: " << opcode.value); diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 9ea9b924..cdf7a13c 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -497,10 +497,15 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.trigger == SfzTrigger::attack); region.parseOpcode({ "trigger", "release" }); REQUIRE(region.trigger == SfzTrigger::release); + REQUIRE(region.loopMode == SfzLoopMode::one_shot); region.parseOpcode({ "trigger", "first" }); REQUIRE(region.trigger == SfzTrigger::first); region.parseOpcode({ "trigger", "legato" }); REQUIRE(region.trigger == SfzTrigger::legato); + region.parseOpcode({ "loop_mode", "no_loop" }); + region.parseOpcode({ "trigger", "release_key" }); + REQUIRE(region.trigger == SfzTrigger::release_key); + REQUIRE(region.loopMode == SfzLoopMode::one_shot); } SECTION("on_locc, on_hicc") From 66bdaafbe7f592aad1603cc7242ba249eab30fab Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 12:35:58 +0200 Subject: [PATCH 132/445] Only override the loop mode if unset --- src/sfizz/Region.cpp | 2 -- src/sfizz/Synth.cpp | 3 +++ tests/FilesT.cpp | 22 ++++++++++++++++++++++ tests/RegionT.cpp | 7 ++----- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index b940031a..2d96835a 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -349,11 +349,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) break; case hash("release"): trigger = SfzTrigger::release; - loopMode = SfzLoopMode::one_shot; break; case hash("release_key"): trigger = SfzTrigger::release_key; - loopMode = SfzLoopMode::one_shot; break; default: DBG("Unknown trigger mode: " << opcode.value); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 0ce7d527..5a0b6103 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -507,6 +507,9 @@ void sfz::Synth::finalizeSfzLoad() region->loopMode = SfzLoopMode::loop_continuous; } + if (region->isRelease() && !region->loopMode) + region->loopMode = SfzLoopMode::one_shot; + if (region->loopRange.getEnd() == Default::loopRange.getEnd()) region->loopRange.setEnd(region->sampleEnd); diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index f92f496c..6c0c6507 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -575,6 +575,28 @@ TEST_CASE("[Files] Looped regions can start at 0") REQUIRE( synth.getRegionView(0)->loopRange == Range { 0, synth.getRegionView(0)->sampleEnd } ); } +TEST_CASE("[Synth] Release triggers automatically sets the loop mode") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/triggers_setting_loops.sfz", R"( + sample=kick.wav pitch_keycenter=69 loop_mode=loop_sustain trigger=release + sample=kick.wav pitch_keycenter=69 loop_mode=loop_sustain trigger=release_key + sample=kick.wav pitch_keycenter=69 trigger=release loop_mode=loop_sustain + sample=kick.wav pitch_keycenter=69 trigger=release_key loop_mode=loop_sustain + sample=looped_flute.wav pitch_keycenter=69 trigger=release_key + sample=kick.wav pitch_keycenter=69 trigger=release_key // These are normal and set to one_shot + sample=kick.wav pitch_keycenter=69 trigger=release + )"); + REQUIRE( synth.getNumRegions() == 7 ); + REQUIRE( synth.getRegionView(0)->loopMode == SfzLoopMode::loop_sustain ); + REQUIRE( synth.getRegionView(1)->loopMode == SfzLoopMode::loop_sustain ); + REQUIRE( synth.getRegionView(2)->loopMode == SfzLoopMode::loop_sustain ); + REQUIRE( synth.getRegionView(3)->loopMode == SfzLoopMode::loop_sustain ); + REQUIRE( synth.getRegionView(4)->loopMode == SfzLoopMode::loop_continuous ); + REQUIRE( synth.getRegionView(5)->loopMode == SfzLoopMode::one_shot ); + REQUIRE( synth.getRegionView(6)->loopMode == SfzLoopMode::one_shot ); +} + TEST_CASE("[Files] Case sentitiveness") { const fs::path sfzFilePath = fs::current_path() / "tests/TestFiles/case_insensitive.sfz"; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index cdf7a13c..9e90b7db 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -497,15 +497,12 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.trigger == SfzTrigger::attack); region.parseOpcode({ "trigger", "release" }); REQUIRE(region.trigger == SfzTrigger::release); - REQUIRE(region.loopMode == SfzLoopMode::one_shot); + region.parseOpcode({ "trigger", "release_key" }); + REQUIRE(region.trigger == SfzTrigger::release_key); region.parseOpcode({ "trigger", "first" }); REQUIRE(region.trigger == SfzTrigger::first); region.parseOpcode({ "trigger", "legato" }); REQUIRE(region.trigger == SfzTrigger::legato); - region.parseOpcode({ "loop_mode", "no_loop" }); - region.parseOpcode({ "trigger", "release_key" }); - REQUIRE(region.trigger == SfzTrigger::release_key); - REQUIRE(region.loopMode == SfzLoopMode::one_shot); } SECTION("on_locc, on_hicc") From f6e0bb8b3f942d59d0373db3f53e464efca72ee3 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 19:12:33 +0200 Subject: [PATCH 133/445] Add special cases for the rate transformation in envelopes For increasing linear ramps, a 0s ramp is an increment of 1.0 For decaying exponential ramps, a 0s ramp is a multiplicator of 0.0f --- src/sfizz/ADSREnvelope.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index b17bce09..78415f8a 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -20,13 +20,18 @@ Type ADSREnvelope::secondsToSamples (Type timeInSeconds) const noexcept template Type ADSREnvelope::secondsToLinRate (Type timeInSeconds) const noexcept { - timeInSeconds = std::max(timeInSeconds, config::virtuallyZero); + if (timeInSeconds == 0) + return 1.0f; + return 1 / (sampleRate * timeInSeconds); }; template Type ADSREnvelope::secondsToExpRate (Type timeInSeconds) const noexcept { + if (timeInSeconds == 0) + return 0.0f; + timeInSeconds = std::max(25e-3, timeInSeconds); return std::exp(-9.0 / (timeInSeconds * sampleRate)); }; From f6f1e68f18ab68112eebc004595f7f76c52d0e45 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 19 Aug 2020 19:13:50 +0200 Subject: [PATCH 134/445] Sustain tweaks: - Separate the sustain value from the sustain threshold after which the envelope goes from decay to sustain - If the sustain value is set to 0, the envelope is freerunning --- src/sfizz/ADSREnvelope.cpp | 10 +++++--- src/sfizz/ADSREnvelope.h | 1 + tests/SynthT.cpp | 50 +++++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index 78415f8a..8d92ea10 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -48,12 +48,14 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, this->hold = secondsToSamples(desc.getHold(state, velocity)); this->peak = 1.0; this->sustain = normalizePercents(desc.getSustain(state, velocity)); - this->sustain = max(this->sustain, config::virtuallyZero); this->start = this->peak * normalizePercents(desc.getStart(state, velocity)); releaseDelay = 0; + sustainThreshold = this->sustain + config::virtuallyZero; shouldRelease = false; - freeRunning = ((region.trigger == SfzTrigger::release) + freeRunning = ( + (region.trigger == SfzTrigger::release) + || (this->sustain == 0.0f) || (region.trigger == SfzTrigger::release_key) || (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator))); currentValue = this->start; @@ -89,7 +91,7 @@ Type ADSREnvelope::getNextValue() noexcept // fallthrough case State::Decay: currentValue *= decayRate; - if (currentValue > sustain) + if (currentValue > sustainThreshold ) return currentValue; currentState = State::Sustain; @@ -159,7 +161,7 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept case State::Decay: while (count < size && (currentValue *= decayRate) > sustain) output[count++] = currentValue; - if (currentValue <= sustain) { + if (currentValue <= sustainThreshold) { currentValue = sustain; currentState = State::Sustain; } diff --git a/src/sfizz/ADSREnvelope.h b/src/sfizz/ADSREnvelope.h index 892388f7..eafd2f90 100644 --- a/src/sfizz/ADSREnvelope.h +++ b/src/sfizz/ADSREnvelope.h @@ -102,6 +102,7 @@ private: Type start { 0 }; Type peak { 0 }; Type sustain { 0 }; + Type sustainThreshold { config::virtuallyZero }; int releaseDelay { 0 }; bool shouldRelease { false }; bool freeRunning { false }; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 5d167385..78f017b6 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1167,6 +1167,55 @@ TEST_CASE("[Synth] end=-1 voices are immediately killed after triggering but the REQUIRE( numPlayingVoices(synth) == 0 ); } +TEST_CASE("[Synth] end=0 voices are immediately killed after triggering but they kill other voices") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path(), R"( + key=60 end=0 sample=*sine + key=61 end=0 sample=*silence + key=62 sample=*sine off_by=2 + key=63 end=0 sample=*saw group=2 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 61, 85); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(1, 63, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 0 ); +} + +TEST_CASE("[Synth] ampeg_sustain = 0 puts the ampeg envelope in free-running mode, which kills the voice almost instantly in most cases") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path(), R"( + key=60 sample=*sine ampeg_sustain=0 + key=61 sample=*sine ampeg_sustain=0 ampeg_attack=0.1 ampeg_decay=0.1 + )"); + + synth.noteOn(0, 60, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + synth.renderBlock(buffer); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOn(0, 61, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + // Render a bit; this does not kill the voice + for (unsigned i = 0; i < 5; ++i) + synth.renderBlock(buffer); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + // Render about half a second + for (unsigned i = 0; i < 100; ++i) + synth.renderBlock(buffer); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); +} + TEST_CASE("[Synth] Off by standard") { sfz::Synth synth; @@ -1222,4 +1271,3 @@ TEST_CASE("[Synth] Off by same note") auto playingVoices = getPlayingVoices(synth); REQUIRE( playingVoices.front()->getRegion()->sampleId.filename() == "*triangle" ); } - From 958d9bb5e87f4606f38d68b25af0421ece09d76b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 20 Aug 2020 02:23:02 +0200 Subject: [PATCH 135/445] Simpler power/envelope followerThis one is much cheaper... --- src/sfizz/Config.h | 2 +- src/sfizz/Voice.cpp | 29 ++++++++++------------------- src/sfizz/Voice.h | 9 +++------ src/sfizz/VoiceStealing.cpp | 14 +++++++------- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 2956dae6..d3c301ba 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -57,7 +57,7 @@ namespace config { constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 }; constexpr float A440 { 440.0 }; constexpr size_t powerHistoryLength { 16 }; - constexpr float filteredEnvelopeCutoff { 5 }; + constexpr float powerFollowerFactor { 10 }; constexpr uint16_t numCCs { 512 }; constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index d7517a3d..8b1a0bed 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -30,9 +30,6 @@ sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources) gainSmoother.setSmoothing(config::gainSmoothing, sampleRate); xfadeSmoother.setSmoothing(config::xfadeSmoothing, sampleRate); - - for (auto & filter : channelEnvelopeFilters) - filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); } sfz::Voice::~Voice() @@ -246,20 +243,19 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept gainSmoother.setSmoothing(config::gainSmoothing, sampleRate); xfadeSmoother.setSmoothing(config::xfadeSmoothing, sampleRate); - for (auto & filter : channelEnvelopeFilters) - filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); - for (WavetableOscillator& osc : waveOscillators) osc.init(sampleRate); for (auto& lfo : lfos) lfo->setSampleRate(sampleRate); + + trackingFactor = samplesPerBlock / sampleRate * config::powerFollowerFactor; } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept { this->samplesPerBlock = samplesPerBlock; - this->minEnvelopeDelay = samplesPerBlock / 2; + this->trackingFactor = samplesPerBlock / sampleRate * config::powerFollowerFactor; } void sfz::Voice::renderBlock(AudioSpan buffer) noexcept @@ -736,10 +732,7 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; - for (auto& f : channelEnvelopeFilters) - f.reset(); - - for (auto& p : smoothedChannelEnvelopes) + for (auto& p : meanChannelPowers) p = 0.0f; filters.clear(); @@ -770,9 +763,9 @@ void sfz::Voice::removeVoiceFromRing() noexcept nextSisterVoice = this; } -float sfz::Voice::getAverageEnvelope() const noexcept +float sfz::Voice::getAveragePower() const noexcept { - return max(smoothedChannelEnvelopes[0], smoothedChannelEnvelopes[1]); + return max(meanChannelPowers[0], meanChannelPowers[1]); } bool sfz::Voice::releasedOrFree() const noexcept @@ -869,16 +862,14 @@ void sfz::Voice::setupOscillatorUnison() void sfz::Voice::updateChannelPowers(AudioSpan buffer) { - assert(smoothedChannelEnvelopes.size() == channelEnvelopeFilters.size()); - assert(buffer.getNumChannels() <= channelEnvelopeFilters.size()); if (buffer.getNumFrames() == 0) return; - for (unsigned i = 0; i < smoothedChannelEnvelopes.size(); ++i) { + const float factor = buffer.getNumFrames() / samplesPerBlock * trackingFactor; + for (unsigned i = 0; i < meanChannelPowers.size(); ++i) { const auto input = buffer.getConstSpan(i); - for (unsigned s = 0; s < buffer.getNumFrames(); ++s) - smoothedChannelEnvelopes[i] = - channelEnvelopeFilters[i].tickLowpass(std::abs(input[s])); + const float meanPower = sfz::meanSquared(input); + meanChannelPowers[i] = meanChannelPowers[i] * (1 - factor) + meanPower * factor; } } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 5b72e799..facd3c1d 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -261,7 +261,7 @@ public: * * @return float */ - float getAverageEnvelope() const noexcept; + float getAveragePower() const noexcept; /** * @brief Get the position of the voice in the source, in samples * @@ -450,7 +450,6 @@ private: FilePromisePtr currentPromise { nullptr }; int samplesPerBlock { config::defaultSamplesPerBlock }; - int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 }; float sampleRate { config::defaultSampleRate }; Resources& resources; @@ -486,10 +485,8 @@ private: Smoother xfadeSmoother; void resetSmoothers() noexcept; - std::array, 2> channelEnvelopeFilters; - std::array smoothedChannelEnvelopes; - - HistoricalBuffer powerHistory { config::powerHistoryLength }; + float trackingFactor { config::defaultSamplesPerBlock / config::defaultSampleRate * config::powerFollowerFactor }; + std::array meanChannelPowers; LEAK_DETECTOR(Voice); }; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 49e1b2d0..0c935df5 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -13,12 +13,12 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept // Start of the voice stealing algorithm absl::c_stable_sort(voices, voiceOrdering); - const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { - return sum + v->getAverageEnvelope(); + const auto sumPower = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { + return sum + v->getAveragePower(); }); - // We are checking the envelope to try and kill voices with relative low contribution + // We are checking the power to try and kill voices with relative low contribution // to the output compared to the rest. - const auto envThreshold = sumEnvelope + const auto powerThreshold = sumPower / static_cast(voices.size()) * config::stealingEnvelopeCoeff; // We are checking the age so that voices have the time to build up attack // This is not perfect because pad-type voices will take a long time to output @@ -37,12 +37,12 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept break; } - float maxEnvelope { 0.0f }; + float maxPower { 0.0f }; SisterVoiceRing::applyToRing(ref, [&](Voice* v) { - maxEnvelope = max(maxEnvelope, v->getAverageEnvelope()); + maxPower = max(maxPower, v->getAveragePower()); }); - if (maxEnvelope < envThreshold) { + if (maxPower < powerThreshold) { returnedVoice = ref; break; } From d649cec10b635613480274476e2dc4b32aca20ee Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 21 Aug 2020 00:24:22 +0200 Subject: [PATCH 136/445] offset_cc were not taken into account --- src/sfizz/Region.cpp | 2 +- tests/RegionT.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 2d96835a..082c2374 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1447,7 +1447,7 @@ uint64_t sfz::Region::getOffset(Oversampling factor) const noexcept uint64_t finalOffset = offset + offsetDistribution(Random::randomGenerator); for (const auto& mod: offsetCC) finalOffset += static_cast(mod.data * midiState.getCCValue(mod.cc)); - return Default::offsetCCRange.clamp(offset + offsetDistribution(Random::randomGenerator)) * static_cast(factor); + return Default::offsetRange.clamp(finalOffset) * static_cast(factor); } float sfz::Region::getDelay() const noexcept diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 9e90b7db..2f430ccd 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1900,3 +1900,21 @@ TEST_CASE("[Region] Release and release key") REQUIRE( region.delayedReleases == expected ); } } + +TEST_CASE("[Region] Offsets with CCs") +{ + MidiState midiState; + Region region { 0, midiState }; + + region.parseOpcode({ "offset_cc4", "255" }); + region.parseOpcode({ "offset", "10" }); + REQUIRE( region.getOffset() == 10 ); + midiState.ccEvent(0, 4, 127_norm); + REQUIRE( region.getOffset() == 265 ); + midiState.ccEvent(0, 4, 100_norm); + REQUIRE( region.getOffset() == 210 ); + midiState.ccEvent(0, 4, 10_norm); + REQUIRE( region.getOffset() == 30 ); + midiState.ccEvent(0, 4, 0); + REQUIRE( region.getOffset() == 10 ); +} From afc9d72c8968f4069f8b123fe3fb4ea94f79a3b9 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 21 Aug 2020 11:17:44 +0200 Subject: [PATCH 137/445] Further changes to the envelope follower- Apply on a single channel- Track attack and release separately --- src/sfizz/Config.h | 9 +++++---- src/sfizz/Voice.cpp | 32 +++++++++++++++++++++----------- src/sfizz/Voice.h | 5 +++-- src/sfizz/VoiceStealing.cpp | 2 +- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index d3c301ba..fc48aab5 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -57,7 +57,8 @@ namespace config { constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 }; constexpr float A440 { 440.0 }; constexpr size_t powerHistoryLength { 16 }; - constexpr float powerFollowerFactor { 10 }; + constexpr float powerFollowerAttackFactor { 100 }; + constexpr float powerFollowerReleaseFactor { 10 }; constexpr uint16_t numCCs { 512 }; constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; @@ -72,10 +73,10 @@ namespace config { */ constexpr float stealingAgeCoeff { 0.5f }; /** - * @brief The threshold for envelope stealing. - * In percentage of the sum of all envelopes. + * @brief The threshold for power stealing. + * In percentage of the sum of all powers. */ - constexpr float stealingEnvelopeCoeff { 0.5f }; + constexpr float stealingPowerCoeff { 0.5f }; constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; constexpr int oscillatorsPerVoice { 9 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 8b1a0bed..54c5ebc2 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -249,13 +249,13 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept for (auto& lfo : lfos) lfo->setSampleRate(sampleRate); - trackingFactor = samplesPerBlock / sampleRate * config::powerFollowerFactor; + attackTrackingFactor = config::powerFollowerAttackFactor / sampleRate; + releaseTrackingFactor = config::powerFollowerReleaseFactor / sampleRate; } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept { this->samplesPerBlock = samplesPerBlock; - this->trackingFactor = samplesPerBlock / sampleRate * config::powerFollowerFactor; } void sfz::Voice::renderBlock(AudioSpan buffer) noexcept @@ -732,8 +732,7 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; - for (auto& p : meanChannelPowers) - p = 0.0f; + meanChannelPower = 0.0f; filters.clear(); equalizers.clear(); @@ -765,7 +764,7 @@ void sfz::Voice::removeVoiceFromRing() noexcept float sfz::Voice::getAveragePower() const noexcept { - return max(meanChannelPowers[0], meanChannelPowers[1]); + return meanChannelPower; } bool sfz::Voice::releasedOrFree() const noexcept @@ -865,12 +864,23 @@ void sfz::Voice::updateChannelPowers(AudioSpan buffer) if (buffer.getNumFrames() == 0) return; - const float factor = buffer.getNumFrames() / samplesPerBlock * trackingFactor; - for (unsigned i = 0; i < meanChannelPowers.size(); ++i) { - const auto input = buffer.getConstSpan(i); - const float meanPower = sfz::meanSquared(input); - meanChannelPowers[i] = meanChannelPowers[i] * (1 - factor) + meanPower * factor; - } + auto tempBuffer = resources.bufferPool.getBuffer(buffer.getNumFrames()); + if (!tempBuffer) + return; + + sfz::copy(buffer.getConstSpan(0), *tempBuffer); + for (unsigned i = 1; i < buffer.getNumChannels(); ++i) + sfz::add(buffer.getConstSpan(i), *tempBuffer); + + const float meanPower = sfz::meanSquared(*tempBuffer); + + const float attackFactor = static_cast(buffer.getNumFrames()) * attackTrackingFactor; + const float releaseFactor = static_cast(buffer.getNumFrames()) * releaseTrackingFactor; + + meanChannelPower = max( + meanChannelPower * (1 - attackFactor) + meanPower * attackFactor, + meanChannelPower * (1 - releaseFactor) + meanPower * releaseFactor + ); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index facd3c1d..c2b3d1c4 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -485,8 +485,9 @@ private: Smoother xfadeSmoother; void resetSmoothers() noexcept; - float trackingFactor { config::defaultSamplesPerBlock / config::defaultSampleRate * config::powerFollowerFactor }; - std::array meanChannelPowers; + float attackTrackingFactor { config::powerFollowerAttackFactor / config::defaultSampleRate }; + float releaseTrackingFactor { config::powerFollowerReleaseFactor / config::defaultSampleRate }; + float meanChannelPower; LEAK_DETECTOR(Voice); }; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 0c935df5..0fa4f1a7 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -19,7 +19,7 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept // We are checking the power to try and kill voices with relative low contribution // to the output compared to the rest. const auto powerThreshold = sumPower - / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + / static_cast(voices.size()) * config::stealingPowerCoeff; // We are checking the age so that voices have the time to build up attack // This is not perfect because pad-type voices will take a long time to output // their sound, but it's reasonable for sounds with a quick attack and longer From dd8134277c45c283d89d25817aa8abfce9ca345c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 21 Aug 2020 11:36:09 +0200 Subject: [PATCH 138/445] Clamp the tracking factors to avoid blowups --- src/sfizz/Voice.cpp | 12 ++++++++++-- src/sfizz/Voice.h | 6 ++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 54c5ebc2..caaa718f 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -237,6 +237,14 @@ void sfz::Voice::registerTempo(int delay, float secondsPerQuarter) noexcept UNUSED(secondsPerQuarter); } +void sfz::Voice::updateTrackingFactor() noexcept +{ + // Protect the envelope follower against blowups + const auto maxTrackingFactor = sampleRate / samplesPerBlock; + attackTrackingFactor = min(config::powerFollowerAttackFactor, maxTrackingFactor) / sampleRate; + releaseTrackingFactor = min(config::powerFollowerReleaseFactor, maxTrackingFactor) / sampleRate; +} + void sfz::Voice::setSampleRate(float sampleRate) noexcept { this->sampleRate = sampleRate; @@ -249,13 +257,13 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept for (auto& lfo : lfos) lfo->setSampleRate(sampleRate); - attackTrackingFactor = config::powerFollowerAttackFactor / sampleRate; - releaseTrackingFactor = config::powerFollowerReleaseFactor / sampleRate; + updateTrackingFactor(); } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept { this->samplesPerBlock = samplesPerBlock; + updateTrackingFactor(); } void sfz::Voice::renderBlock(AudioSpan buffer) noexcept diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index c2b3d1c4..5881e79c 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -485,6 +485,12 @@ private: Smoother xfadeSmoother; void resetSmoothers() noexcept; + /** + * @brief Update and clamp the tracking factors to ensure the power + * follower does not blow up. + * + */ + void updateTrackingFactor() noexcept; float attackTrackingFactor { config::powerFollowerAttackFactor / config::defaultSampleRate }; float releaseTrackingFactor { config::powerFollowerReleaseFactor / config::defaultSampleRate }; float meanChannelPower; From a71c1eb0f377846f05491be78a515e0f51db13e4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 21 Aug 2020 14:39:55 +0200 Subject: [PATCH 139/445] Allow modulations to trigger at a frame-accurate instant (LFO) --- src/sfizz/LFO.cpp | 5 +++-- src/sfizz/LFO.h | 2 +- src/sfizz/Voice.cpp | 2 +- src/sfizz/modulations/ModGenerator.h | 3 ++- src/sfizz/modulations/ModMatrix.cpp | 6 +++--- src/sfizz/modulations/ModMatrix.h | 2 +- src/sfizz/modulations/sources/Controller.cpp | 3 ++- src/sfizz/modulations/sources/Controller.h | 2 +- src/sfizz/modulations/sources/LFO.cpp | 4 ++-- src/sfizz/modulations/sources/LFO.h | 2 +- tests/LFOT.cpp | 2 +- tests/PlotLFO.cpp | 2 +- 12 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/sfizz/LFO.cpp b/src/sfizz/LFO.cpp index 33d31f70..06c823b5 100644 --- a/src/sfizz/LFO.cpp +++ b/src/sfizz/LFO.cpp @@ -49,7 +49,7 @@ void LFO::configure(const LFODescription* desc) impl_->desc_ = desc ? desc : &LFODescription::getDefault(); } -void LFO::start() +void LFO::start(unsigned triggerDelay) { Impl& impl = *impl_; const LFODescription& desc = *impl.desc_; @@ -59,7 +59,8 @@ void LFO::start() impl.sampleHoldMem_.fill(0.0f); const float delay = desc.delay; - impl.delayFramesLeft_ = (delay > 0) ? static_cast(std::ceil(sampleRate * delay)) : 0u; + size_t delayFrames = (delay > 0) ? static_cast(std::ceil(sampleRate * delay)) : 0u; + impl.delayFramesLeft_ = triggerDelay + delayFrames; impl.fadePosition_ = (desc.fade > 0) ? 0.0f : 1.0f; } diff --git a/src/sfizz/LFO.h b/src/sfizz/LFO.h index 3d75cad3..7a43fd13 100644 --- a/src/sfizz/LFO.h +++ b/src/sfizz/LFO.h @@ -67,7 +67,7 @@ public: Start processing a LFO as a region is triggered. Prepares the delay, phases, fade-in, etc.. */ - void start(); + void start(unsigned triggerDelay); /** Process a cycle of the oscillator. diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 3321d761..56d7e6ce 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -150,7 +150,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate); - resources.modMatrix.initVoice(id, region->getId()); + resources.modMatrix.initVoice(id, region->getId(), delay); } int sfz::Voice::getCurrentSampleQuality() const noexcept diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h index 2229ec15..8c7a89e8 100644 --- a/src/sfizz/modulations/ModGenerator.h +++ b/src/sfizz/modulations/ModGenerator.h @@ -36,8 +36,9 @@ public: * * @param sourceKey identifier of the source to initialize * @param voiceId the particular voice to initialize, if per-voice + * @param delay the frame time when it happens */ - virtual void init(const ModKey& sourceKey, NumericId voiceId) = 0; + virtual void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) = 0; /** * @brief Generate a cycle of the modulator diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 694a9cc0..913f3095 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -196,18 +196,18 @@ void ModMatrix::init() for (Impl::Source &source : impl.sources_) { const int flags = source.key.flags(); if (flags & kModIsPerCycle) - source.gen->init(source.key, {}); + source.gen->init(source.key, {}, 0); } } -void ModMatrix::initVoice(NumericId voiceId, NumericId regionId) +void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, unsigned delay) { Impl& impl = *impl_; for (Impl::Source &source : impl.sources_) { const int flags = source.key.flags(); if ((flags & kModIsPerVoice) && source.key.region() == regionId) - source.gen->init(source.key, voiceId); + source.gen->init(source.key, voiceId, delay); } } diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 0a451bec..21592360 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -106,7 +106,7 @@ public: * @brief Reinitialize modulation source for a given voice. * This must be called first after a voice enters active state. */ - void initVoice(NumericId voiceId, NumericId regionId); + void initVoice(NumericId voiceId, NumericId regionId, unsigned delay); /** * @brief Start modulation processing for the entire cycle. diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp index 692b7556..1e611220 100644 --- a/src/sfizz/modulations/sources/Controller.cpp +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -49,9 +49,10 @@ void ControllerSource::setSamplesPerBlock(unsigned count) (void)count; } -void ControllerSource::init(const ModKey& sourceKey, NumericId voiceId) +void ControllerSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) { (void)voiceId; + (void)delay; const ModKey::Parameters p = sourceKey.parameters(); if (p.smooth > 0) { diff --git a/src/sfizz/modulations/sources/Controller.h b/src/sfizz/modulations/sources/Controller.h index 1dfe26cd..d5320cf3 100644 --- a/src/sfizz/modulations/sources/Controller.h +++ b/src/sfizz/modulations/sources/Controller.h @@ -18,7 +18,7 @@ public: ~ControllerSource(); void setSampleRate(double sampleRate) override; void setSamplesPerBlock(unsigned count) override; - void init(const ModKey& sourceKey, NumericId voiceId) override; + void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; private: diff --git a/src/sfizz/modulations/sources/LFO.cpp b/src/sfizz/modulations/sources/LFO.cpp index 0f604898..2f06c34f 100644 --- a/src/sfizz/modulations/sources/LFO.cpp +++ b/src/sfizz/modulations/sources/LFO.cpp @@ -19,7 +19,7 @@ LFOSource::LFOSource(Synth &synth) { } -void LFOSource::init(const ModKey& sourceKey, NumericId voiceId) +void LFOSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) { Synth& synth = *synth_; unsigned lfoIndex = sourceKey.parameters().N; @@ -38,7 +38,7 @@ void LFOSource::init(const ModKey& sourceKey, NumericId voiceId) LFO* lfo = voice->getLFO(lfoIndex); lfo->configure(®ion->lfos[lfoIndex]); - lfo->start(); + lfo->start(delay); } void LFOSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) diff --git a/src/sfizz/modulations/sources/LFO.h b/src/sfizz/modulations/sources/LFO.h index 83a1e30f..cf7e702f 100644 --- a/src/sfizz/modulations/sources/LFO.h +++ b/src/sfizz/modulations/sources/LFO.h @@ -13,7 +13,7 @@ class Synth; class LFOSource : public ModGenerator { public: explicit LFOSource(Synth &synth); - void init(const ModKey& sourceKey, NumericId voiceId) override; + void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; private: diff --git a/tests/LFOT.cpp b/tests/LFOT.cpp index 84c2795d..2271c2a8 100644 --- a/tests/LFOT.cpp +++ b/tests/LFOT.cpp @@ -31,7 +31,7 @@ static bool computeLFO(DataPoints& dp, const fs::path& sfzPath, double sampleRat std::vector outputMemory(numLfos * numFrames); for (size_t l = 0; l < numLfos; ++l) { - lfos[l].start(); + lfos[l].start(0); } std::vector> lfoOutputs(numLfos); diff --git a/tests/PlotLFO.cpp b/tests/PlotLFO.cpp index 06ccc553..8ce0aff5 100644 --- a/tests/PlotLFO.cpp +++ b/tests/PlotLFO.cpp @@ -119,7 +119,7 @@ int main(int argc, char* argv[]) std::vector outputMemory(numLfos * numFrames); for (size_t l = 0; l < numLfos; ++l) { - lfos[l].start(); + lfos[l].start(0); } std::vector> lfoOutputs(numLfos); From 0481862b100e1de828500c1ced38b00ebaaa68bf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 21 Aug 2020 14:49:36 +0200 Subject: [PATCH 140/445] Allow the modulations to be notified of release --- src/sfizz/Voice.cpp | 2 ++ src/sfizz/modulations/ModGenerator.h | 9 +++++++++ src/sfizz/modulations/ModMatrix.cpp | 11 +++++++++++ src/sfizz/modulations/ModMatrix.h | 6 ++++++ 4 files changed, 28 insertions(+) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 56d7e6ce..1b644066 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -174,6 +174,8 @@ void sfz::Voice::release(int delay) noexcept } else { egEnvelope.startRelease(delay); } + + resources.modMatrix.releaseVoice(id, region->getId(), delay); } void sfz::Voice::off(int delay) noexcept diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h index 8c7a89e8..3d7aed12 100644 --- a/src/sfizz/modulations/ModGenerator.h +++ b/src/sfizz/modulations/ModGenerator.h @@ -40,6 +40,15 @@ public: */ virtual void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) = 0; + /** + * @brief Send the generator a release notification. + * + * @param sourceKey identifier of the source to release + * @param voiceId the particular voice to initialize, if per-voice + * @param delay the frame time when it happens + */ + virtual void release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) { (void)sourceKey; (void)voiceId; (void)delay; } + /** * @brief Generate a cycle of the modulator * diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 913f3095..9290ba2d 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -211,6 +211,17 @@ void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, } } +void ModMatrix::releaseVoice(NumericId voiceId, NumericId regionId, unsigned delay) +{ + Impl& impl = *impl_; + + for (Impl::Source &source : impl.sources_) { + const int flags = source.key.flags(); + if ((flags & kModIsPerVoice) && source.key.region() == regionId) + source.gen->release(source.key, voiceId, delay); + } +} + void ModMatrix::beginCycle(unsigned numFrames) { Impl& impl = *impl_; diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 21592360..211cf06f 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -108,6 +108,12 @@ public: */ void initVoice(NumericId voiceId, NumericId regionId, unsigned delay); + /** + * @brief Release modulation source for a given voice. + * This must be called when a voice enters released state. + */ + void releaseVoice(NumericId voiceId, NumericId regionId, unsigned delay); + /** * @brief Start modulation processing for the entire cycle. * This clears all the buffers. From e1a0855b9dc6f1a6f285a8a484d1820903db0cd3 Mon Sep 17 00:00:00 2001 From: JP Cimalando Date: Fri, 21 Aug 2020 15:18:34 +0200 Subject: [PATCH 141/445] Add @kinwie as a contributor --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 20318099..f2b6beb1 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -8,3 +8,4 @@ Contributors to `sfizz`, in chronologic order: - Michael Willis (2020) - Jean-Pierre Cimalando (2020) - Tobiasz "unfa" KaroÅ„ (2020) +- Kinwie (2020) From d240f4f63f42ce899dfd3ada8a310b374e8bde49 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 21 Aug 2020 20:34:49 +0200 Subject: [PATCH 142/445] vst: set slider default values --- vst/SfizzVstEditor.h | 1 + 1 file changed, 1 insertion(+) diff --git a/vst/SfizzVstEditor.h b/vst/SfizzVstEditor.h index 7b6e8392..21cf8a87 100644 --- a/vst/SfizzVstEditor.h +++ b/vst/SfizzVstEditor.h @@ -66,6 +66,7 @@ private: auto* p = static_cast(getController()->getParameterObject(id)); c->setMin(p->getMin()); c->setMax(p->getMax()); + c->setDefaultValue(p->toPlain(p->getInfo().defaultNormalizedValue)); } enum { From cb144ad42f5903ecba79fa0888eee31965c095e9 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 27 Aug 2020 04:56:56 +0200 Subject: [PATCH 143/445] Move the power follower to its own file --- dpf.mk | 1 + src/CMakeLists.txt | 2 + src/sfizz/PowerFollower.cpp | 75 +++++++++++++++++++++++++++++++++++++ src/sfizz/PowerFollower.h | 37 ++++++++++++++++++ src/sfizz/Voice.cpp | 43 +++------------------ src/sfizz/Voice.h | 12 ++---- 6 files changed, 123 insertions(+), 47 deletions(-) create mode 100644 src/sfizz/PowerFollower.cpp create mode 100644 src/sfizz/PowerFollower.h diff --git a/dpf.mk b/dpf.mk index 5507e79b..2ee507e8 100644 --- a/dpf.mk +++ b/dpf.mk @@ -103,6 +103,7 @@ SFIZZ_SOURCES = \ src/sfizz/Parser.cpp \ src/sfizz/parser/Parser.cpp \ src/sfizz/parser/ParserPrivate.cpp \ + src/sfizz/PowerFollower.cpp \ src/sfizz/Region.cpp \ src/sfizz/RTSemaphore.cpp \ src/sfizz/ScopedFTZ.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e97af140..ed19f1b6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -81,6 +81,7 @@ set (SFIZZ_HEADERS sfizz/Oversampler.h sfizz/Panning.h sfizz/PolyphonyGroup.h + sfizz/PowerFollower.h sfizz/railsback/2-1.h sfizz/railsback/4-1.h sfizz/railsback/4-2.h @@ -138,6 +139,7 @@ set (SFIZZ_SOURCES sfizz/Effects.cpp sfizz/LFO.cpp sfizz/LFODescription.cpp + sfizz/PowerFollower.cpp sfizz/modulations/ModId.cpp sfizz/modulations/ModKey.cpp sfizz/modulations/ModKeyHash.cpp diff --git a/src/sfizz/PowerFollower.cpp b/src/sfizz/PowerFollower.cpp new file mode 100644 index 00000000..56ccb86e --- /dev/null +++ b/src/sfizz/PowerFollower.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "PowerFollower.h" +#include "Defaults.h" +#include "SIMDHelpers.h" +#include + +namespace sfz { + +PowerFollower::PowerFollower() + : sampleRate_(config::defaultSampleRate), + samplesPerBlock_(config::defaultSamplesPerBlock), + tempBuffer_(new float[config::defaultSamplesPerBlock]) +{ + updateTrackingFactor(); +} + +void PowerFollower::setSampleRate(float sampleRate) noexcept +{ + if (sampleRate_ != sampleRate) { + sampleRate_ = sampleRate; + updateTrackingFactor(); + } +} + +void PowerFollower::setSamplesPerBlock(unsigned samplesPerBlock) +{ + if (samplesPerBlock_ != samplesPerBlock) { + tempBuffer_.reset(new float[samplesPerBlock]); + samplesPerBlock_ = samplesPerBlock; + updateTrackingFactor(); + } +} + +void PowerFollower::process(AudioSpan buffer) noexcept +{ + size_t numFrames = buffer.getNumFrames(); + if (numFrames == 0) + return; + + absl::Span tempBuffer(tempBuffer_.get(), numFrames); + + copy(buffer.getConstSpan(0), tempBuffer); + for (unsigned i = 1; i < buffer.getNumChannels(); ++i) + add(buffer.getConstSpan(i), tempBuffer); + + const float meanPower = meanSquared(tempBuffer); + + const float attackFactor = static_cast(buffer.getNumFrames()) * attackTrackingFactor_; + const float releaseFactor = static_cast(buffer.getNumFrames()) * releaseTrackingFactor_; + + meanChannelPower_ = max( + meanChannelPower_ * (1 - attackFactor) + meanPower * attackFactor, + meanChannelPower_ * (1 - releaseFactor) + meanPower * releaseFactor + ); +} + +void PowerFollower::clear() noexcept +{ + meanChannelPower_ = 0; +} + +void PowerFollower::updateTrackingFactor() noexcept +{ + // Protect the envelope follower against blowups + const auto maxTrackingFactor = sampleRate_ / samplesPerBlock_; + attackTrackingFactor_ = min(config::powerFollowerAttackFactor, maxTrackingFactor) / sampleRate_; + releaseTrackingFactor_ = min(config::powerFollowerReleaseFactor, maxTrackingFactor) / sampleRate_; +} + +} // namespace sfz diff --git a/src/sfizz/PowerFollower.h b/src/sfizz/PowerFollower.h new file mode 100644 index 00000000..2deff3d5 --- /dev/null +++ b/src/sfizz/PowerFollower.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "AudioSpan.h" +#include + +namespace sfz { + +class PowerFollower { +public: + PowerFollower(); + void setSampleRate(float sampleRate) noexcept; + void setSamplesPerBlock(unsigned samplesPerBlock); + void process(AudioSpan buffer) noexcept; + void clear() noexcept; + float getAveragePower() const noexcept { return meanChannelPower_; } + +private: + void updateTrackingFactor() noexcept; + +private: + float sampleRate_ {}; + unsigned samplesPerBlock_ {}; + + std::unique_ptr tempBuffer_; + + float attackTrackingFactor_ {}; + float releaseTrackingFactor_ {}; + + float meanChannelPower_ {}; +}; + +} // namespace sfz diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index caaa718f..53e399f4 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -237,14 +237,6 @@ void sfz::Voice::registerTempo(int delay, float secondsPerQuarter) noexcept UNUSED(secondsPerQuarter); } -void sfz::Voice::updateTrackingFactor() noexcept -{ - // Protect the envelope follower against blowups - const auto maxTrackingFactor = sampleRate / samplesPerBlock; - attackTrackingFactor = min(config::powerFollowerAttackFactor, maxTrackingFactor) / sampleRate; - releaseTrackingFactor = min(config::powerFollowerReleaseFactor, maxTrackingFactor) / sampleRate; -} - void sfz::Voice::setSampleRate(float sampleRate) noexcept { this->sampleRate = sampleRate; @@ -257,13 +249,13 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept for (auto& lfo : lfos) lfo->setSampleRate(sampleRate); - updateTrackingFactor(); + powerFollower.setSampleRate(sampleRate); } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept { this->samplesPerBlock = samplesPerBlock; - updateTrackingFactor(); + powerFollower.setSamplesPerBlock(samplesPerBlock); } void sfz::Voice::renderBlock(AudioSpan buffer) noexcept @@ -299,7 +291,7 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept if (!egEnvelope.isSmoothing()) switchState(State::cleanMeUp); - updateChannelPowers(buffer); + powerFollower.process(buffer); age += buffer.getNumFrames(); if (triggerDelay) { @@ -740,7 +732,7 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; - meanChannelPower = 0.0f; + powerFollower.clear(); filters.clear(); equalizers.clear(); @@ -772,7 +764,7 @@ void sfz::Voice::removeVoiceFromRing() noexcept float sfz::Voice::getAveragePower() const noexcept { - return meanChannelPower; + return powerFollower.getAveragePower(); } bool sfz::Voice::releasedOrFree() const noexcept @@ -867,31 +859,6 @@ void sfz::Voice::setupOscillatorUnison() #endif } -void sfz::Voice::updateChannelPowers(AudioSpan buffer) -{ - if (buffer.getNumFrames() == 0) - return; - - auto tempBuffer = resources.bufferPool.getBuffer(buffer.getNumFrames()); - if (!tempBuffer) - return; - - sfz::copy(buffer.getConstSpan(0), *tempBuffer); - for (unsigned i = 1; i < buffer.getNumChannels(); ++i) - sfz::add(buffer.getConstSpan(i), *tempBuffer); - - const float meanPower = sfz::meanSquared(*tempBuffer); - - const float attackFactor = static_cast(buffer.getNumFrames()) * attackTrackingFactor; - const float releaseFactor = static_cast(buffer.getNumFrames()) * releaseTrackingFactor; - - meanChannelPower = max( - meanChannelPower * (1 - attackFactor) + meanPower * attackFactor, - meanChannelPower * (1 - releaseFactor) + meanPower * releaseFactor - ); -} - - void sfz::Voice::switchState(State s) { if (s != state) { diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 5881e79c..0aec391e 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -15,6 +15,7 @@ #include "AudioSpan.h" #include "LeakDetector.h" #include "OnePoleFilter.h" +#include "PowerFollower.h" #include "NumericId.h" #include "absl/types/span.h" #include @@ -485,15 +486,8 @@ private: Smoother xfadeSmoother; void resetSmoothers() noexcept; - /** - * @brief Update and clamp the tracking factors to ensure the power - * follower does not blow up. - * - */ - void updateTrackingFactor() noexcept; - float attackTrackingFactor { config::powerFollowerAttackFactor / config::defaultSampleRate }; - float releaseTrackingFactor { config::powerFollowerReleaseFactor / config::defaultSampleRate }; - float meanChannelPower; + PowerFollower powerFollower; + LEAK_DETECTOR(Voice); }; From 4c3db6c2c8547d75e65c0233145a091fad08f631 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 27 Aug 2020 05:32:39 +0200 Subject: [PATCH 144/445] Add benchmark --- benchmarks/BM_powerFollower.cpp | 60 +++++++++++++++++++++++++++++++++ benchmarks/CMakeLists.txt | 2 ++ 2 files changed, 62 insertions(+) create mode 100644 benchmarks/BM_powerFollower.cpp diff --git a/benchmarks/BM_powerFollower.cpp b/benchmarks/BM_powerFollower.cpp new file mode 100644 index 00000000..4358f596 --- /dev/null +++ b/benchmarks/BM_powerFollower.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "PowerFollower.h" +#include "AudioBuffer.h" +#include "Config.h" +#include +#include + +class PowerFollowerFixture : public benchmark::Fixture { +public: + PowerFollowerFixture() + { + inputSignal_ = sfz::AudioBuffer(2, numFrames); + auto leftSignal = inputSignal_.getSpan(0); + auto rightSignal = inputSignal_.getSpan(1); + float phase = 0; + for (size_t i = 0; i < numFrames; ++i) { + constexpr float k2pi = 2.0 * M_PI; + leftSignal[i] = std::sin(k2pi * phase); + rightSignal[i] = std::cos(k2pi * phase); + phase += 440.0f / sfz::config::defaultSampleRate; + phase -= static_cast(phase); + } + } + + void SetUp(const ::benchmark::State& state) + { + auto blockSize = static_cast(state.range(0)); + follower_.setSampleRate(sfz::config::defaultSampleRate); + follower_.setSamplesPerBlock(blockSize); + follower_.clear(); + } + + void TearDown(const ::benchmark::State& /* state */) + { + } + + static constexpr size_t numFrames = 65536; + sfz::PowerFollower follower_; + sfz::AudioBuffer inputSignal_; +}; + +constexpr size_t PowerFollowerFixture::numFrames; + +BENCHMARK_DEFINE_F(PowerFollowerFixture, Follower) (benchmark::State& state) +{ + sfz::AudioSpan inputSignal(inputSignal_); + for (auto _ : state) { + auto& follower = follower_; + auto blockSize = static_cast(state.range(0)); + for (size_t i = 0; i < numFrames; i += blockSize) + follower.process(inputSignal.subspan(i, blockSize)); + } +} + +BENCHMARK_REGISTER_F(PowerFollowerFixture, Follower)->RangeMultiplier(2)->Range(1 << 5, 1 << 12); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 71fd0b14..4efb3d82 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -66,6 +66,8 @@ sfizz_add_benchmark(bm_logger BM_logger.cpp) target_link_libraries(bm_logger PRIVATE sfizz::sfizz) sfizz_add_benchmark(bm_smoothers BM_smoothers.cpp) target_link_libraries(bm_smoothers PRIVATE sfizz::sfizz) +sfizz_add_benchmark(bm_powerFollower BM_powerFollower.cpp) +target_link_libraries(bm_powerFollower PRIVATE sfizz::sfizz) if (TARGET sfizz-samplerate) sfizz_add_benchmark(bm_resample BM_resample.cpp ${BENCHMARK_SIMD_SOURCES}) From a5c72375a984c3d4fd2202c1fb18a9c9aedf8319 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 27 Aug 2020 08:08:37 +0200 Subject: [PATCH 145/445] Move the div operation out of the SIMD helper --- benchmarks/BM_meanSquared.cpp | 8 ++++---- src/sfizz/SIMDHelpers.cpp | 12 ++++++------ src/sfizz/SIMDHelpers.h | 30 +++++++++++++++++++++++++----- src/sfizz/simd/HelpersSSE.cpp | 4 ++-- src/sfizz/simd/HelpersSSE.h | 2 +- src/sfizz/simd/HelpersScalar.h | 4 ++-- tests/SIMDHelpersT.cpp | 8 ++++---- 7 files changed, 44 insertions(+), 24 deletions(-) diff --git a/benchmarks/BM_meanSquared.cpp b/benchmarks/BM_meanSquared.cpp index 0b1c159e..34c29a8f 100644 --- a/benchmarks/BM_meanSquared.cpp +++ b/benchmarks/BM_meanSquared.cpp @@ -34,7 +34,7 @@ BENCHMARK_DEFINE_F(MeanSquaredArray, Scalar) (benchmark::State& state) { for (auto _ : state) { - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, false); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, false); auto result = sfz::meanSquared(input); benchmark::DoNotOptimize(result); } @@ -44,7 +44,7 @@ BENCHMARK_DEFINE_F(MeanSquaredArray, SIMD) (benchmark::State& state) { for (auto _ : state) { - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, true); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, true); auto result = sfz::meanSquared(input); benchmark::DoNotOptimize(result); } @@ -54,7 +54,7 @@ BENCHMARK_DEFINE_F(MeanSquaredArray, Scalar_Unaligned) (benchmark::State& state) { for (auto _ : state) { - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, false); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, false); auto result = sfz::meanSquared(absl::MakeSpan(input).subspan(1)); benchmark::DoNotOptimize(result); } @@ -64,7 +64,7 @@ BENCHMARK_DEFINE_F(MeanSquaredArray, SIMD_Unaligned) (benchmark::State& state) { for (auto _ : state) { - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, true); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, true); auto result = sfz::meanSquared(absl::MakeSpan(input).subspan(1)); benchmark::DoNotOptimize(result); } diff --git a/src/sfizz/SIMDHelpers.cpp b/src/sfizz/SIMDHelpers.cpp index c86cecf8..c0287772 100644 --- a/src/sfizz/SIMDHelpers.cpp +++ b/src/sfizz/SIMDHelpers.cpp @@ -39,7 +39,7 @@ struct SIMDDispatch { decltype(&cumsumScalar) cumsum = &cumsumScalar; decltype(&diffScalar) diff = &diffScalar; decltype(&meanScalar) mean = &meanScalar; - decltype(&meanSquaredScalar) meanSquared = &meanSquaredScalar; + decltype(&sumSquaresScalar) sumSquares = &sumSquaresScalar; decltype(&clampAllScalar) clampAll = &clampAllScalar; decltype(&allWithinScalar) allWithin = &allWithinScalar; @@ -85,7 +85,7 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(cumsum) SIMD_OP(diff) SIMD_OP(mean) - SIMD_OP(meanSquared) + SIMD_OP(sumSquares) SIMD_OP(clampAll) SIMD_OP(allWithin) } @@ -122,7 +122,7 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(cumsum) SIMD_OP(diff) SIMD_OP(mean) - SIMD_OP(meanSquared) + SIMD_OP(sumSquares) SIMD_OP(clampAll) SIMD_OP(allWithin) } @@ -163,7 +163,7 @@ void SIMDDispatch::resetStatus() setStatus(SIMDOps::diff, false); setStatus(SIMDOps::sfzInterpolationCast, true); setStatus(SIMDOps::mean, false); - setStatus(SIMDOps::meanSquared, false); + setStatus(SIMDOps::sumSquares, false); setStatus(SIMDOps::upsampling, true); setStatus(SIMDOps::clampAll, false); setStatus(SIMDOps::allWithin, true); @@ -292,9 +292,9 @@ float mean(const float* vector, unsigned size) noexcept } template <> -float meanSquared(const float* vector, unsigned size) noexcept +float sumSquares(const float* vector, unsigned size) noexcept { - return simdDispatch().meanSquared(vector, size); + return simdDispatch().sumSquares(vector, size); } template <> diff --git a/src/sfizz/SIMDHelpers.h b/src/sfizz/SIMDHelpers.h index c0a27c09..ae34f732 100644 --- a/src/sfizz/SIMDHelpers.h +++ b/src/sfizz/SIMDHelpers.h @@ -57,7 +57,7 @@ enum class SIMDOps { diff, sfzInterpolationCast, mean, - meanSquared, + sumSquares, upsampling, clampAll, allWithin, @@ -515,7 +515,7 @@ T mean(absl::Span vector) noexcept } /** - * @brief Computes the mean squared of a span + * @brief Computes the sum of squares of a span * * @tparam T the underlying type * @tparam SIMD use the SIMD version or the scalar version @@ -523,13 +523,33 @@ T mean(absl::Span vector) noexcept * @return T */ template -T meanSquared(const T* vector, unsigned size) noexcept +T sumSquares(const T* vector, unsigned size) noexcept { - meanSquaredScalar(vector, size); + return sumSquaresScalar(vector, size); } template <> -float meanSquared(const float* vector, unsigned size) noexcept; +float sumSquares(const float* vector, unsigned size) noexcept; + +template +T sumSquares(absl::Span vector) noexcept +{ + return sumSquares(vector.data(), vector.size()); +} + +/** + * @brief Computes the mean squared of a span + * + * @tparam T the underlying type + * @param vector + * @return T + */ +template +T meanSquared(const T* vector, unsigned size) noexcept +{ + T sum = sumSquares(vector, size); + return sum / size; +} template T meanSquared(absl::Span vector) noexcept diff --git a/src/sfizz/simd/HelpersSSE.cpp b/src/sfizz/simd/HelpersSSE.cpp index 3ae8b984..d17288e7 100644 --- a/src/sfizz/simd/HelpersSSE.cpp +++ b/src/sfizz/simd/HelpersSSE.cpp @@ -370,7 +370,7 @@ float meanSSE(const float* vector, unsigned size) noexcept return result / static_cast(size); } -float meanSquaredSSE(const float* vector, unsigned size) noexcept +float sumSquaresSSE(const float* vector, unsigned size) noexcept { const auto sentinel = vector + size; @@ -404,7 +404,7 @@ float meanSquaredSSE(const float* vector, unsigned size) noexcept vector++; } - return result / static_cast(size); + return result; } void cumsumSSE(const float* input, float* output, unsigned size) noexcept diff --git a/src/sfizz/simd/HelpersSSE.h b/src/sfizz/simd/HelpersSSE.h index cff28650..5046d914 100644 --- a/src/sfizz/simd/HelpersSSE.h +++ b/src/sfizz/simd/HelpersSSE.h @@ -22,7 +22,7 @@ void subtractSSE(const float* input, float* output, unsigned size) noexcept; void subtract1SSE(float value, float* output, unsigned size) noexcept; void copySSE(const float* input, float* output, unsigned size) noexcept; float meanSSE(const float* vector, unsigned size) noexcept; -float meanSquaredSSE(const float* vector, unsigned size) noexcept; +float sumSquaresSSE(const float* vector, unsigned size) noexcept; void cumsumSSE(const float* input, float* output, unsigned size) noexcept; void diffSSE(const float* input, float* output, unsigned size) noexcept; void clampAllSSE(float* input, float low, float high, unsigned size) noexcept; diff --git a/src/sfizz/simd/HelpersScalar.h b/src/sfizz/simd/HelpersScalar.h index d5ac1162..871a9ff6 100644 --- a/src/sfizz/simd/HelpersScalar.h +++ b/src/sfizz/simd/HelpersScalar.h @@ -142,7 +142,7 @@ T meanScalar(const T* vector, unsigned size) noexcept } template -T meanSquaredScalar(const T* vector, unsigned size) noexcept +T sumSquaresScalar(const T* vector, unsigned size) noexcept { T result{ 0.0 }; if (size == 0) @@ -154,7 +154,7 @@ T meanSquaredScalar(const T* vector, unsigned size) noexcept vector++; } - return result / static_cast(size); + return result; } template diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index 805bb0f9..f0ef8277 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -690,9 +690,9 @@ TEST_CASE("[Helpers] Mean (SIMD vs scalar)") TEST_CASE("[Helpers] Mean Squared") { std::array input { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f }; - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, false); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, false); REQUIRE(sfz::meanSquared(input) == 38.5f); - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, true); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, true); REQUIRE(sfz::meanSquared(input) == 38.5f); } @@ -700,9 +700,9 @@ TEST_CASE("[Helpers] Mean Squared (SIMD vs scalar)") { std::vector input(medBufferSize); absl::c_iota(input, 0.0f); - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, false); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, false); auto scalarResult = sfz::meanSquared(input); - sfz::setSIMDOpStatus(sfz::SIMDOps::meanSquared, true); + sfz::setSIMDOpStatus(sfz::SIMDOps::sumSquares, true); auto simdResult = sfz::meanSquared(input); REQUIRE( scalarResult == Approx(simdResult).margin(1e-3) ); } From 732222b8d36c3e09c7896b96cef777197953c910 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 27 Aug 2020 09:05:05 +0200 Subject: [PATCH 146/445] Add a reference follower to the benchmark --- benchmarks/BM_powerFollower.cpp | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/benchmarks/BM_powerFollower.cpp b/benchmarks/BM_powerFollower.cpp index 4358f596..6444e0e6 100644 --- a/benchmarks/BM_powerFollower.cpp +++ b/benchmarks/BM_powerFollower.cpp @@ -33,6 +33,10 @@ public: follower_.setSampleRate(sfz::config::defaultSampleRate); follower_.setSamplesPerBlock(blockSize); follower_.clear(); + + // + refFollower_.init(sfz::config::defaultSampleRate); + refFollower_.clear(); } void TearDown(const ::benchmark::State& /* state */) @@ -42,10 +46,70 @@ public: static constexpr size_t numFrames = 65536; sfz::PowerFollower follower_; sfz::AudioBuffer inputSignal_; + + // + struct ReferenceFollower { + /* + import("stdfaust.lib"); + process = (_, _) : + : an.amp_follower_ud(att, rel) with { att = 5e-3; rel = 200e-3; }; + */ + + void init(float sampleRate) + { + fConst0 = std::min(192000.0f, std::max(1.0f, float(sampleRate))); + fConst1 = std::exp((0.0f - (200.0f / fConst0))); + fConst2 = (1.0f - fConst1); + fConst3 = std::exp((0.0f - (5.0f / fConst0))); + fConst4 = (1.0f - fConst3); + } + void clear() + { + for (int l0 = 0; (l0 < 2); l0 = (l0 + 1)) { + fRec1[l0] = 0.0f; + } + for (int l1 = 0; (l1 < 2); l1 = (l1 + 1)) { + fRec0[l1] = 0.0f; + } + } + float process(float input0, float input1) + { + float fTemp0 = std::fabs((float(input0) + float(input1))); + fRec1[0] = std::max(fTemp0, ((fConst3 * fRec1[1]) + (fConst4 * fTemp0))); + fRec0[0] = ((fConst1 * fRec0[1]) + (fConst2 * fRec1[0])); + float output = fRec0[0]; + fRec1[1] = fRec1[0]; + fRec0[1] = fRec0[0]; + return output; + } + + float fConst0; + float fConst1; + float fConst2; + float fConst3; + float fConst4; + float fRec1[2]; + float fRec0[2]; + }; + ReferenceFollower refFollower_; }; constexpr size_t PowerFollowerFixture::numFrames; +BENCHMARK_DEFINE_F(PowerFollowerFixture, ReferenceFollower) (benchmark::State& state) +{ + sfz::AudioSpan inputSignal(inputSignal_); + auto input0 = inputSignal.getConstSpan(0); + auto input1 = inputSignal.getConstSpan(1); + + for (auto _ : state) { + auto& follower = refFollower_; + float output = 0; + for (size_t i = 0, n = inputSignal.getNumFrames(); i < n; ++i) + output = follower.process(input0[i], input1[i]); + benchmark::DoNotOptimize(output); + } +} + BENCHMARK_DEFINE_F(PowerFollowerFixture, Follower) (benchmark::State& state) { sfz::AudioSpan inputSignal(inputSignal_); @@ -57,4 +121,5 @@ BENCHMARK_DEFINE_F(PowerFollowerFixture, Follower) (benchmark::State& state) } } +BENCHMARK_REGISTER_F(PowerFollowerFixture, ReferenceFollower)->Range(1, 1); BENCHMARK_REGISTER_F(PowerFollowerFixture, Follower)->RangeMultiplier(2)->Range(1 << 5, 1 << 12); From b1f1fc40423d013d1430325b064edfa8de278fea Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 27 Aug 2020 09:12:42 +0200 Subject: [PATCH 147/445] Power follower using a fixed block size --- src/sfizz/Config.h | 1 + src/sfizz/PowerFollower.cpp | 49 ++++++++++++++++++++++++++++--------- src/sfizz/PowerFollower.h | 6 +++-- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index fc48aab5..67821b02 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -57,6 +57,7 @@ namespace config { constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 }; constexpr float A440 { 440.0 }; constexpr size_t powerHistoryLength { 16 }; + constexpr size_t powerFollowerStep { 512 }; constexpr float powerFollowerAttackFactor { 100 }; constexpr float powerFollowerReleaseFactor { 10 }; constexpr uint16_t numCCs { 512 }; diff --git a/src/sfizz/PowerFollower.cpp b/src/sfizz/PowerFollower.cpp index 56ccb86e..06807c7a 100644 --- a/src/sfizz/PowerFollower.cpp +++ b/src/sfizz/PowerFollower.cpp @@ -42,26 +42,51 @@ void PowerFollower::process(AudioSpan buffer) noexcept if (numFrames == 0) return; - absl::Span tempBuffer(tempBuffer_.get(), numFrames); + /// + constexpr size_t step = config::powerFollowerStep; + float currentPower = currentPower_; + float currentSum = currentSum_; + size_t currentCount = currentCount_; - copy(buffer.getConstSpan(0), tempBuffer); - for (unsigned i = 1; i < buffer.getNumChannels(); ++i) - add(buffer.getConstSpan(i), tempBuffer); + const float attackFactor = static_cast(numFrames) * attackTrackingFactor_; + const float releaseFactor = static_cast(numFrames) * releaseTrackingFactor_; - const float meanPower = meanSquared(tempBuffer); + /// + size_t index = 0; + while (index < numFrames) { + size_t blockSize = std::min(step - currentCount, numFrames - index); + absl::Span tempBuffer(tempBuffer_.get(), blockSize); - const float attackFactor = static_cast(buffer.getNumFrames()) * attackTrackingFactor_; - const float releaseFactor = static_cast(buffer.getNumFrames()) * releaseTrackingFactor_; + copy(buffer.getConstSpan(0).subspan(index, blockSize), tempBuffer); + for (unsigned i = 1, n = buffer.getNumChannels(); i < n; ++i) + add(buffer.getConstSpan(i).subspan(index, blockSize), tempBuffer); - meanChannelPower_ = max( - meanChannelPower_ * (1 - attackFactor) + meanPower * attackFactor, - meanChannelPower_ * (1 - releaseFactor) + meanPower * releaseFactor - ); + currentSum += sumSquares(tempBuffer); + currentCount += blockSize; + + if (currentCount == step) { + const float meanPower = currentSum / step; + currentPower = max( + currentPower * (1 - attackFactor) + meanPower * attackFactor, + currentPower * (1 - releaseFactor) + meanPower * releaseFactor); + currentSum = 0; + currentCount = 0; + } + + index += blockSize; + } + + /// + currentPower_ = currentPower; + currentSum_ = currentSum; + currentCount_ = currentCount; } void PowerFollower::clear() noexcept { - meanChannelPower_ = 0; + currentPower_ = 0; + currentSum_ = 0; + currentCount_ = 0; } void PowerFollower::updateTrackingFactor() noexcept diff --git a/src/sfizz/PowerFollower.h b/src/sfizz/PowerFollower.h index 2deff3d5..1febd9f1 100644 --- a/src/sfizz/PowerFollower.h +++ b/src/sfizz/PowerFollower.h @@ -17,7 +17,7 @@ public: void setSamplesPerBlock(unsigned samplesPerBlock); void process(AudioSpan buffer) noexcept; void clear() noexcept; - float getAveragePower() const noexcept { return meanChannelPower_; } + float getAveragePower() const noexcept { return currentPower_; } private: void updateTrackingFactor() noexcept; @@ -31,7 +31,9 @@ private: float attackTrackingFactor_ {}; float releaseTrackingFactor_ {}; - float meanChannelPower_ {}; + float currentPower_ {}; + float currentSum_ = 0; + size_t currentCount_ = 0; }; } // namespace sfz From 298befa753ad91c189c94bcbf93d04343b0b1d5c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 27 Aug 2020 09:39:41 +0200 Subject: [PATCH 148/445] Express follower AR in seconds --- src/sfizz/Config.h | 4 ++-- src/sfizz/PowerFollower.cpp | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 67821b02..e56548fc 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -58,8 +58,8 @@ namespace config { constexpr float A440 { 440.0 }; constexpr size_t powerHistoryLength { 16 }; constexpr size_t powerFollowerStep { 512 }; - constexpr float powerFollowerAttackFactor { 100 }; - constexpr float powerFollowerReleaseFactor { 10 }; + constexpr float powerFollowerAttackTime { 5e-3f }; + constexpr float powerFollowerReleaseTime { 200e-3f }; constexpr uint16_t numCCs { 512 }; constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; diff --git a/src/sfizz/PowerFollower.cpp b/src/sfizz/PowerFollower.cpp index 06807c7a..f2eb05e5 100644 --- a/src/sfizz/PowerFollower.cpp +++ b/src/sfizz/PowerFollower.cpp @@ -32,7 +32,6 @@ void PowerFollower::setSamplesPerBlock(unsigned samplesPerBlock) if (samplesPerBlock_ != samplesPerBlock) { tempBuffer_.reset(new float[samplesPerBlock]); samplesPerBlock_ = samplesPerBlock; - updateTrackingFactor(); } } @@ -48,8 +47,8 @@ void PowerFollower::process(AudioSpan buffer) noexcept float currentSum = currentSum_; size_t currentCount = currentCount_; - const float attackFactor = static_cast(numFrames) * attackTrackingFactor_; - const float releaseFactor = static_cast(numFrames) * releaseTrackingFactor_; + const float attackFactor = attackTrackingFactor_; + const float releaseFactor = releaseTrackingFactor_; /// size_t index = 0; @@ -67,8 +66,8 @@ void PowerFollower::process(AudioSpan buffer) noexcept if (currentCount == step) { const float meanPower = currentSum / step; currentPower = max( - currentPower * (1 - attackFactor) + meanPower * attackFactor, - currentPower * (1 - releaseFactor) + meanPower * releaseFactor); + currentPower * attackFactor + meanPower * (1 - attackFactor), + currentPower * releaseFactor + meanPower * (1 - releaseFactor)); currentSum = 0; currentCount = 0; } @@ -92,9 +91,8 @@ void PowerFollower::clear() noexcept void PowerFollower::updateTrackingFactor() noexcept { // Protect the envelope follower against blowups - const auto maxTrackingFactor = sampleRate_ / samplesPerBlock_; - attackTrackingFactor_ = min(config::powerFollowerAttackFactor, maxTrackingFactor) / sampleRate_; - releaseTrackingFactor_ = min(config::powerFollowerReleaseFactor, maxTrackingFactor) / sampleRate_; + attackTrackingFactor_ = std::exp(-1.0f / ((config::powerFollowerAttackTime / config::powerFollowerStep) * sampleRate_)); + releaseTrackingFactor_ = std::exp(-1.0f / ((config::powerFollowerReleaseTime / config::powerFollowerStep) * sampleRate_)); } } // namespace sfz From 9f95bc4c3e7927878e043bf60c83d4693522d7eb Mon Sep 17 00:00:00 2001 From: Olivier Humbert Date: Mon, 31 Aug 2020 13:40:25 +0200 Subject: [PATCH 149/445] Update sfizz.ttl.in - update French translation --- lv2/sfizz.ttl.in | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 582c4a53..b0934ca6 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -39,7 +39,7 @@ midnam:update a lv2:Feature . a pg:Group ; lv2:symbol "status" ; lv2:name "status", - "Status"@fr . + "Statut"@fr . <@LV2PLUGIN_URI@:sfzfile> a lv2:Parameter ; @@ -93,7 +93,8 @@ midnam:update a lv2:Feature . lv2:designation lv2:control ; lv2:index 0 ; lv2:symbol "control" ; - lv2:name "Control" + lv2:name "Control", + "Contrôle"@fr ; ] , [ a lv2:OutputPort, atom:AtomPort ; atom:bufferType atom:Sequence ; @@ -101,7 +102,8 @@ midnam:update a lv2:Feature . lv2:designation lv2:control ; lv2:index 1 ; lv2:symbol "notify" ; - lv2:name "Notify" ; + lv2:name "Notify", + "Notification"@fr ; ] , [ a lv2:AudioPort, lv2:OutputPort ; lv2:index 2 ; From 998a50fa8eeb62ab0bd1307435dc4776a61e0f7a Mon Sep 17 00:00:00 2001 From: JP Cimalando Date: Tue, 1 Sep 2020 07:58:24 +0200 Subject: [PATCH 150/445] A minor update of the abseil URL [ci skip] --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 1ed52774..7a652206 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "external/abseil-cpp"] path = external/abseil-cpp - url = https://github.com/abseil/abseil-cpp + url = https://github.com/abseil/abseil-cpp.git branch = lts_2020_02_25 shallow = true [submodule "vst/external/VST_SDK/VST3_SDK/base"] From 0c8a57e8432475537464f6ecf098305e3eef7640 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 2 Sep 2020 00:36:49 +0200 Subject: [PATCH 151/445] Enable fast-math only on sfizz DSP targets --- benchmarks/CMakeLists.txt | 1 + cmake/SfizzConfig.cmake | 7 ++++++- src/CMakeLists.txt | 2 ++ tests/CMakeLists.txt | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 4efb3d82..cba645f3 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -32,6 +32,7 @@ macro(sfizz_add_benchmark TARGET) target_link_libraries ("${TARGET}" PRIVATE atomic) endif() target_include_directories("${TARGET}" PRIVATE ../src/sfizz ../src/external) + sfizz_enable_fast_math("${TARGET}") endmacro() sfizz_add_benchmark(bm_opf_high_vs_low BM_OPF_high_vs_low.cpp) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 16c805bd..fc69cbbd 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -32,7 +32,6 @@ endif() if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Wall) add_compile_options(-Wextra) - add_compile_options(-ffast-math) add_compile_options(-fno-omit-frame-pointer) # For debugging purposes if (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(i.86|x86_64)$") add_compile_options(-msse2) @@ -43,6 +42,12 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endif() +function(sfizz_enable_fast_math NAME) + if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options("${NAME}" PRIVATE "-ffast-math") + endif() +endfunction() + add_library(sfizz-sndfile INTERFACE) if (SFIZZ_USE_VCPKG OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ed19f1b6..0837e2ac 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -217,6 +217,7 @@ endif() if (SFIZZ_RELEASE_ASSERTS) target_compile_definitions (sfizz_static PRIVATE "SFIZZ_ENABLE_RELEASE_ASSERT=1") endif() +sfizz_enable_fast_math(sfizz_static) if (NOT MSVC) install (TARGETS sfizz_static @@ -257,6 +258,7 @@ if (SFIZZ_SHARED) target_compile_definitions(sfizz_shared PRIVATE SFIZZ_EXPORT_SYMBOLS) set_target_properties (sfizz_shared PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME sfizz) sfizz_enable_lto_if_needed(sfizz_shared) + sfizz_enable_fast_math(sfizz_shared) if (NOT MSVC) install (TARGETS sfizz_shared diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 76cb7ae9..61b16360 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -47,6 +47,7 @@ set(SFIZZ_TEST_SOURCES add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) target_link_libraries(sfizz_tests PRIVATE sfizz::sfizz) sfizz_enable_lto_if_needed(sfizz_tests) +sfizz_enable_fast_math(sfizz_tests) # target_link_libraries(sfizz_tests PRIVATE absl::strings absl::str_format absl::flat_hash_map cnpy absl::span absl::algorithm) find_package(PkgConfig) From 1f58b371517f987f73a784f672deff43dfb61c01 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 2 Sep 2020 01:12:12 +0200 Subject: [PATCH 152/445] travis: change the build order --- .travis.yml | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8b92e35a..7286a566 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,8 +40,26 @@ jobs: install: .travis/download_cmake.sh script: .travis/script_test.sh - - name: "Windows mingw32" + - name: "macOS" stage: "Build" + os: osx + env: + - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" + install: .travis/install_osx.sh + script: .travis/script_osx.sh + after_success: .travis/prepare_osx.sh + + - name: "MOD devices arm" + env: + - CONTAINER=jpcima/mod-plugin-builder + - CROSS_COMPILE=moddevices-arm + - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-moddevices" + before_install: .travis/before_install_moddevices.sh + install: .travis/install_moddevices.sh + script: .travis/script_moddevices.sh + after_success: .travis/prepare_tarball.sh + + - name: "Windows mingw32" env: - CROSS_COMPILE=mingw32 - CONTAINER=archlinux @@ -127,25 +145,6 @@ jobs: script: .travis/script_plugins.sh after_success: .travis/prepare_tarball.sh - - name: "macOS" - os: osx - env: - - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" - install: .travis/install_osx.sh - script: .travis/script_osx.sh - after_success: .travis/prepare_osx.sh - - - name: "MOD devices arm" - stage: "Build" - env: - - CONTAINER=jpcima/mod-plugin-builder - - CROSS_COMPILE=moddevices-arm - - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-moddevices" - before_install: .travis/before_install_moddevices.sh - install: .travis/install_moddevices.sh - script: .travis/script_moddevices.sh - after_success: .travis/prepare_tarball.sh - - stage: "Deploy" name: "Source packaging" if: (tag IS present) AND (branch = master) AND (type = push) From 1460cac41ac129d2dde7c337dad717d6dd1ff6c1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 10:09:52 +0200 Subject: [PATCH 153/445] Force vcpkg update in attempt to fix appveyor --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index d9dd9573..f1110ee1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,6 +12,10 @@ install: - cmd: set PATH=C:\Program Files (x86)\Inno Setup 6;%PATH% - cmd: if %platform%==Win32 set VCPKG_TRIPLET=x86-windows-static - cmd: if %platform%==x64 set VCPKG_TRIPLET=x64-windows-static +- cmd: cd c:\tools\vcpkg\ +- cmd: git pull +- cmd: .\bootstrap-vcpkg.bat +- cmd: cd %APPVEYOR_BUILD_FOLDER% - cmd: vcpkg install libsndfile:%VCPKG_TRIPLET% before_build: From 58ca88da176a6a0bd44ed110c1058ad3df2e50e1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 10:53:16 +0200 Subject: [PATCH 154/445] Attempt at fixing the use of vcpkg sndfile --- cmake/SfizzConfig.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index fc69cbbd..95b462e6 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -51,10 +51,10 @@ endfunction() add_library(sfizz-sndfile INTERFACE) if (SFIZZ_USE_VCPKG OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - find_package(LibSndFile REQUIRED) + find_package(SndFile CONFIG REQUIRED) find_path(SNDFILE_INCLUDE_DIR sndfile.hh) target_include_directories(sfizz-sndfile INTERFACE "${SNDFILE_INCLUDE_DIR}") - target_link_libraries(sfizz-sndfile INTERFACE sndfile-static) + target_link_libraries(sfizz-sndfile INTERFACE SndFile::sndfile) else() find_package(PkgConfig REQUIRED) pkg_check_modules(SNDFILE "sndfile" REQUIRED) From e2b8b21967d3f809b2c7f26458a961f1efb46e4f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 29 Aug 2020 04:54:18 +0200 Subject: [PATCH 155/445] Separate the editor code from VST --- .gitmodules | 2 +- .travis.yml | 2 + .travis/script_mingw.sh | 4 +- .travis/script_moddevices.sh | 2 +- .travis/script_plugins.sh | 3 +- .travis/script_test.sh | 2 +- CMakeLists.txt | 13 +- cmake/LV2Config.cmake | 16 + cmake/SfizzConfig.cmake | 12 +- editor/CMakeLists.txt | 16 + editor/cmake/Vstgui.cmake | 209 ++++ .../VST3_SDK => editor/external}/vstgui4 | 0 editor/src/editor/EditIds.cpp | 32 + editor/src/editor/EditIds.h | 37 + editor/src/editor/Editor.cpp | 977 ++++++++++++++++++ editor/src/editor/Editor.h | 30 + editor/src/editor/EditorController.h | 43 + {vst => editor/src/editor}/GUIComponents.cpp | 3 + {vst => editor/src/editor}/GUIComponents.h | 3 + editor/src/editor/utility/vstgui_after.h | 9 + editor/src/editor/utility/vstgui_before.h | 12 + lv2/CMakeLists.txt | 2 +- vst/CMakeLists.txt | 5 +- vst/SfizzVstController.cpp | 18 +- vst/SfizzVstEditor.cpp | 842 ++------------- vst/SfizzVstEditor.h | 94 +- vst/SfizzVstProcessor.cpp | 6 +- vst/SfizzVstState.h | 6 +- vst/cmake/Vst3.cmake | 219 +--- 29 files changed, 1557 insertions(+), 1062 deletions(-) create mode 100644 editor/CMakeLists.txt create mode 100644 editor/cmake/Vstgui.cmake rename {vst/external/VST_SDK/VST3_SDK => editor/external}/vstgui4 (100%) create mode 100644 editor/src/editor/EditIds.cpp create mode 100644 editor/src/editor/EditIds.h create mode 100644 editor/src/editor/Editor.cpp create mode 100644 editor/src/editor/Editor.h create mode 100644 editor/src/editor/EditorController.h rename {vst => editor/src/editor}/GUIComponents.cpp (93%) rename {vst => editor/src/editor}/GUIComponents.h (91%) create mode 100644 editor/src/editor/utility/vstgui_after.h create mode 100644 editor/src/editor/utility/vstgui_before.h diff --git a/.gitmodules b/.gitmodules index 7a652206..1247ca5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,6 +16,6 @@ url = https://github.com/sfztools/vst3_public_sdk.git shallow = true [submodule "vst/external/VST_SDK/VST3_SDK/vstgui4"] - path = vst/external/VST_SDK/VST3_SDK/vstgui4 + path = editor/external/vstgui4 url = https://github.com/sfztools/vstgui.git shallow = true diff --git a/.travis.yml b/.travis.yml index 7286a566..1fb67fdf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,6 +110,7 @@ jobs: env: - INSTALL_DIR="sfizz-plugins-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" - ENABLE_VST_PLUGIN=OFF + - ENABLE_LV2_UI=OFF addons: apt: packages: @@ -125,6 +126,7 @@ jobs: env: - INSTALL_DIR="sfizz-plugins-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" - ENABLE_VST_PLUGIN=ON + - ENABLE_LV2_UI=ON addons: apt: packages: diff --git a/.travis/script_mingw.sh b/.travis/script_mingw.sh index 2f1f545f..e09db92a 100755 --- a/.travis/script_mingw.sh +++ b/.travis/script_mingw.sh @@ -9,7 +9,7 @@ if [[ ${CROSS_COMPILE} == "mingw32" ]]; then -DENABLE_LTO=OFF \ -DSFIZZ_JACK=OFF \ -DSFIZZ_VST=ON \ - -DSFIZZ_STATIC_LIBSNDFILE=ON \ + -DSFIZZ_STATIC_DEPENDENCIES=ON \ -DCMAKE_CXX_STANDARD=17 \ .. buildenv make -j$(nproc) @@ -18,7 +18,7 @@ elif [[ ${CROSS_COMPILE} == "mingw64" ]]; then -DENABLE_LTO=OFF \ -DSFIZZ_JACK=OFF \ -DSFIZZ_VST=ON \ - -DSFIZZ_STATIC_LIBSNDFILE=ON \ + -DSFIZZ_STATIC_DEPENDENCIES=ON \ -DCMAKE_CXX_STANDARD=17 \ .. buildenv make -j$(nproc) diff --git a/.travis/script_moddevices.sh b/.travis/script_moddevices.sh index 649dbb3d..278b7045 100755 --- a/.travis/script_moddevices.sh +++ b/.travis/script_moddevices.sh @@ -7,5 +7,5 @@ mkdir -p build/${INSTALL_DIR} && cd build buildenv mod-plugin-builder /usr/local/bin/cmake \ -DSFIZZ_SYSTEM_PROCESSOR=armv7-a \ - -DCMAKE_BUILD_TYPE=Release -DSFIZZ_JACK=OFF .. + -DCMAKE_BUILD_TYPE=Release -DSFIZZ_JACK=OFF -DSFIZZ_LV2_UI=OFF .. buildenv mod-plugin-builder make -j$(nproc) diff --git a/.travis/script_plugins.sh b/.travis/script_plugins.sh index 89eaf8ac..66e6b09f 100755 --- a/.travis/script_plugins.sh +++ b/.travis/script_plugins.sh @@ -5,9 +5,10 @@ mkdir -p build/${INSTALL_DIR} && cd build cmake -DCMAKE_BUILD_TYPE=Release \ -DSFIZZ_JACK=OFF \ -DSFIZZ_VST="$ENABLE_VST_PLUGIN" \ + -DSFIZZ_LV2_UI="$ENABLE_LV2_UI" \ -DSFIZZ_TESTS=OFF \ -DSFIZZ_SHARED=OFF \ - -DSFIZZ_STATIC_LIBSNDFILE=ON \ + -DSFIZZ_STATIC_DEPENDENCIES=ON \ -DCMAKE_CXX_STANDARD=17 \ .. make -j$(nproc) diff --git a/.travis/script_test.sh b/.travis/script_test.sh index 844ec9b9..1a23a092 100755 --- a/.travis/script_test.sh +++ b/.travis/script_test.sh @@ -6,7 +6,7 @@ cmake -DCMAKE_BUILD_TYPE=Release \ -DSFIZZ_JACK=OFF \ -DSFIZZ_TESTS=ON \ -DSFIZZ_SHARED=OFF \ - -DSFIZZ_STATIC_LIBSNDFILE=OFF \ + -DSFIZZ_STATIC_DEPENDENCIES=OFF \ -DSFIZZ_LV2=OFF \ -DCMAKE_CXX_STANDARD=17 \ .. diff --git a/CMakeLists.txt b/CMakeLists.txt index cbc3945b..0158e93a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,16 +19,11 @@ include (SfizzConfig) # Build Options set (BUILD_TESTING OFF CACHE BOOL "Disable Abseil's tests [default: OFF]") -# On macOS add the needed directories for both library and jack client -if (APPLE) - include_directories (SYSTEM /usr/local/opt/libsndfile/include) - link_directories (/usr/local/opt/libsndfile/lib) -endif() - option (ENABLE_LTO "Enable Link Time Optimization [default: ON]" ON) option (SFIZZ_JACK "Enable JACK stand-alone build [default: ON]" ON) option (SFIZZ_RENDER "Enable renderer of SMF files [default: ON]" ON) option (SFIZZ_LV2 "Enable LV2 plug-in build [default: ON]" ON) +option (SFIZZ_LV2_UI "Enable LV2 plug-in user interface [default: ON]" ON) option (SFIZZ_VST "Enable VST plug-in build [default: OFF]" OFF) option (SFIZZ_AU "Enable AU plug-in build [default: OFF]" OFF) option (SFIZZ_BENCHMARKS "Enable benchmarks build [default: OFF]" OFF) @@ -36,7 +31,7 @@ option (SFIZZ_TESTS "Enable tests build [default: OFF]" OFF) option (SFIZZ_DEVTOOLS "Enable developer tools build [default: OFF]" OFF) option (SFIZZ_SHARED "Enable shared library build [default: ON]" ON) option (SFIZZ_USE_VCPKG "Assume that sfizz is build using vcpkg [default: OFF]" OFF) -option (SFIZZ_STATIC_LIBSNDFILE "Link libsndfile statically [default: OFF]" OFF) +option (SFIZZ_STATIC_DEPENDENCIES "Link dependencies statically [default: OFF]" OFF) option (SFIZZ_RELEASE_ASSERTS "Forced assertions in release builds [default: OFF]" OFF) # Don't use IPO in non Release builds @@ -51,6 +46,10 @@ add_subdirectory (src) # Optional targets add_subdirectory (clients) +if ((SFIZZ_LV2 AND SFIZZ_LV2_UI) OR SFIZZ_VST) + add_subdirectory (editor) +endif() + if (SFIZZ_LV2) add_subdirectory (lv2) endif() diff --git a/cmake/LV2Config.cmake b/cmake/LV2Config.cmake index 95c54bdf..6b9d9b76 100644 --- a/cmake/LV2Config.cmake +++ b/cmake/LV2Config.cmake @@ -14,6 +14,22 @@ else() set (LV2PLUGIN_SPDX_LICENSE_ID "ISC") endif() +if(SFIZZ_LV2_UI) + set(LV2PLUGIN_IF_ENABLE_UI "") +else() + set(LV2PLUGIN_IF_ENABLE_UI "#") +endif() + +if(WIN32) + set(LV2_UI_TYPE "WindowsUI") +elseif(APPLE) + set(LV2_UI_TYPE "CocoaUI") +elseif(HAIKU) + set(LV2_UI_TYPE "BeUI") +else() + set(LV2_UI_TYPE "X11UI") +endif() + if (MSVC) set (LV2PLUGIN_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lv2" CACHE STRING "Install destination for LV2 bundle [default: ${CMAKE_INSTALL_PREFIX}/lv2}]") diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 95b462e6..ae6aec60 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -17,6 +17,11 @@ if (WIN32) add_compile_definitions(_WIN32_WINNT=0x601) endif() +# Do not define macros `min` and `max` +if (WIN32) + add_compile_definitions(NOMINMAX) +endif() + # The variable CMAKE_SYSTEM_PROCESSOR is incorrect on Visual studio... # see https://gitlab.kitware.com/cmake/cmake/issues/15170 @@ -48,6 +53,7 @@ function(sfizz_enable_fast_math NAME) endif() endfunction() +# The sndfile library add_library(sfizz-sndfile INTERFACE) if (SFIZZ_USE_VCPKG OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") @@ -59,11 +65,12 @@ else() find_package(PkgConfig REQUIRED) pkg_check_modules(SNDFILE "sndfile" REQUIRED) target_include_directories(sfizz-sndfile INTERFACE ${SNDFILE_INCLUDE_DIRS}) - if (SFIZZ_STATIC_LIBSNDFILE) + if (SFIZZ_STATIC_DEPENDENCIES) target_link_libraries(sfizz-sndfile INTERFACE ${SNDFILE_STATIC_LIBRARIES}) else() target_link_libraries(sfizz-sndfile INTERFACE ${SNDFILE_LIBRARIES}) endif() + link_directories(${SNDFILE_LIBRARY_DIRS}) endif() @@ -112,12 +119,13 @@ Build using LTO: ${ENABLE_LTO} Build as shared library: ${SFIZZ_SHARED} Build JACK stand-alone client: ${SFIZZ_JACK} Build LV2 plug-in: ${SFIZZ_LV2} +Build LV2 user interface: ${SFIZZ_LV2_UI} Build VST plug-in: ${SFIZZ_VST} Build AU plug-in: ${SFIZZ_AU} Build benchmarks: ${SFIZZ_BENCHMARKS} Build tests: ${SFIZZ_TESTS} Use vcpkg: ${SFIZZ_USE_VCPKG} -Statically link libsndfile: ${SFIZZ_STATIC_LIBSNDFILE} +Statically link dependencies: ${SFIZZ_STATIC_DEPENDENCIES} Link libatomic: ${SFIZZ_LINK_LIBATOMIC} Use clang libc++: ${USE_LIBCPP} Release asserts: ${SFIZZ_RELEASE_ASSERTS} diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt new file mode 100644 index 00000000..977f36a4 --- /dev/null +++ b/editor/CMakeLists.txt @@ -0,0 +1,16 @@ +set(VSTGUI_BASEDIR "${CMAKE_CURRENT_SOURCE_DIR}/external/vstgui4") +include("cmake/Vstgui.cmake") + +# editor +add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL + src/editor/EditIds.h + src/editor/EditIds.cpp + src/editor/Editor.h + src/editor/Editor.cpp + src/editor/EditorController.h + src/editor/GUIComponents.h + src/editor/GUIComponents.cpp + src/editor/utility/vstgui_after.h + src/editor/utility/vstgui_before.h) +target_include_directories(sfizz_editor PUBLIC "src") +target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui absl::strings) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake new file mode 100644 index 00000000..92f85f03 --- /dev/null +++ b/editor/cmake/Vstgui.cmake @@ -0,0 +1,209 @@ +add_library(sfizz-vstgui STATIC EXCLUDE_FROM_ALL + "${VSTGUI_BASEDIR}/vstgui/lib/animation/animations.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/animation/animator.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/animation/timingfunctions.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cbitmap.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cbitmapfilter.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/ccolor.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cdatabrowser.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cdrawcontext.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cdrawmethods.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cdropsource.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cfileselector.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cfont.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cframe.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cgradientview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cgraphicspath.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/clayeredviewcontainer.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/clinestyle.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/coffscreencontext.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cautoanimation.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cbuttons.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/ccolorchooser.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/ccontrol.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cfontchooser.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cknob.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/clistcontrol.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cmoviebitmap.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cmoviebutton.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/coptionmenu.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cparamdisplay.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cscrollbar.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/csearchtextedit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/csegmentbutton.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cslider.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cspecialdigit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/csplashscreen.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cstringlist.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cswitch.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/ctextedit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/ctextlabel.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cvumeter.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/controls/cxypad.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/copenglview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cpoint.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/crect.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/crowcolumnview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cscrollview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cshadowviewcontainer.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/csplitview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cstring.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/ctabview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/ctooltipsupport.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cviewcontainer.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/cvstguitimer.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/genericstringlistdatabrowsersource.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/genericoptionmenu.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/vstguidebug.cpp") + +if(WIN32) + target_sources(sfizz-vstgui PRIVATE + "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/fileresourceinputstream.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/direct2d/d2dbitmap.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/direct2d/d2ddrawcontext.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/direct2d/d2dfont.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/direct2d/d2dgraphicspath.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32datapackage.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32dragging.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32frame.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32openglview.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32optionmenu.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32support.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32textedit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/winfileselector.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/winstring.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/wintimer.cpp") +elseif(APPLE) + target_sources(sfizz-vstgui PRIVATE + "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/fileresourceinputstream.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/genericoptionmenu.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/generictextedit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/carbon/hiviewframe.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/carbon/hiviewoptionmenu.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/carbon/hiviewtextedit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/caviewlayer.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cfontmac.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cgbitmap.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cgdrawcontext.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/autoreleasepool.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/cocoahelpers.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/cocoaopenglview.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/cocoatextedit.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/nsviewdraggingsession.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/nsviewframe.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/nsviewoptionmenu.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macclipboard.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macfileselector.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macglobals.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macstring.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/mactimer.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/quartzgraphicspath.cpp") +else() + target_sources(sfizz-vstgui PRIVATE + "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/fileresourceinputstream.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/generictextedit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairobitmap.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairocontext.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairofont.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairogradient.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairopath.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/linuxstring.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11fileselector.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11frame.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11platform.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11timer.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11utils.cpp") +endif() + +target_include_directories(sfizz-vstgui PUBLIC "${VSTGUI_BASEDIR}") + +if(WIN32) + if (NOT MSVC) + # autolinked on MSVC with pragmas + find_library(OPENGL32_LIBRARY "opengl32") + find_library(D2D1_LIBRARY "d2d1") + find_library(DWRITE_LIBRARY "dwrite") + find_library(DWMAPI_LIBRARY "dwmapi") + find_library(WINDOWSCODECS_LIBRARY "windowscodecs") + find_library(SHLWAPI_LIBRARY "shlwapi") + target_link_libraries(sfizz-vstgui PRIVATE + "${OPENGL32_LIBRARY}" + "${D2D1_LIBRARY}" + "${DWRITE_LIBRARY}" + "${DWMAPI_LIBRARY}" + "${WINDOWSCODECS_LIBRARY}" + "${SHLWAPI_LIBRARY}") + endif() +elseif(APPLE) + find_library(APPLE_COREFOUNDATION_LIBRARY "CoreFoundation") + find_library(APPLE_FOUNDATION_LIBRARY "Foundation") + find_library(APPLE_COCOA_LIBRARY "Cocoa") + find_library(APPLE_OPENGL_LIBRARY "OpenGL") + find_library(APPLE_ACCELERATE_LIBRARY "Accelerate") + find_library(APPLE_QUARTZCORE_LIBRARY "QuartzCore") + find_library(APPLE_CARBON_LIBRARY "Carbon") + find_library(APPLE_AUDIOTOOLBOX_LIBRARY "AudioToolbox") + find_library(APPLE_COREAUDIO_LIBRARY "CoreAudio") + find_library(APPLE_COREMIDI_LIBRARY "CoreMIDI") + target_link_libraries(sfizz-vstgui PRIVATE + "${APPLE_COREFOUNDATION_LIBRARY}" + "${APPLE_FOUNDATION_LIBRARY}" + "${APPLE_COCOA_LIBRARY}" + "${APPLE_OPENGL_LIBRARY}" + "${APPLE_ACCELERATE_LIBRARY}" + "${APPLE_QUARTZCORE_LIBRARY}" + "${APPLE_CARBON_LIBRARY}" + "${APPLE_AUDIOTOOLBOX_LIBRARY}" + "${APPLE_COREAUDIO_LIBRARY}" + "${APPLE_COREMIDI_LIBRARY}") +else() + find_package(X11 REQUIRED) + find_package(Freetype REQUIRED) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBXCB REQUIRED xcb) + pkg_check_modules(LIBXCB_UTIL REQUIRED xcb-util) + pkg_check_modules(LIBXCB_CURSOR REQUIRED xcb-cursor) + pkg_check_modules(LIBXCB_KEYSYMS REQUIRED xcb-keysyms) + pkg_check_modules(LIBXCB_XKB REQUIRED xcb-xkb) + pkg_check_modules(LIBXKB_COMMON REQUIRED xkbcommon) + pkg_check_modules(LIBXKB_COMMON_X11 REQUIRED xkbcommon-x11) + pkg_check_modules(CAIRO REQUIRED cairo) + pkg_check_modules(FONTCONFIG REQUIRED fontconfig) + target_include_directories(sfizz-vstgui PRIVATE + ${X11_INCLUDE_DIRS} + ${FREETYPE_INCLUDE_DIRS} + ${LIBXCB_INCLUDE_DIRS} + ${LIBXCB_UTIL_INCLUDE_DIRS} + ${LIBXCB_CURSOR_INCLUDE_DIRS} + ${LIBXCB_KEYSYMS_INCLUDE_DIRS} + ${LIBXCB_XKB_INCLUDE_DIRS} + ${LIBXKB_COMMON_INCLUDE_DIRS} + ${LIBXKB_COMMON_X11_INCLUDE_DIRS} + ${CAIRO_INCLUDE_DIRS} + ${FONTCONFIG_INCLUDE_DIRS}) + target_link_libraries(sfizz-vstgui PRIVATE + ${X11_LIBRARIES} + ${FREETYPE_LIBRARIES} + ${LIBXCB_LIBRARIES} + ${LIBXCB_UTIL_LIBRARIES} + ${LIBXCB_CURSOR_LIBRARIES} + ${LIBXCB_KEYSYMS_LIBRARIES} + ${LIBXCB_XKB_LIBRARIES} + ${LIBXKB_COMMON_LIBRARIES} + ${LIBXKB_COMMON_X11_LIBRARIES} + ${CAIRO_LIBRARIES} + ${FONTCONFIG_LIBRARIES}) + find_library(DL_LIBRARY "dl") + if(DL_LIBRARY) + target_link_libraries(sfizz-vstgui PRIVATE "${DL_LIBRARY}") + endif() +endif() + +if(${CMAKE_BUILD_TYPE} MATCHES "Debug") + target_compile_definitions(sfizz-vstgui PRIVATE "DEVELOPMENT") +endif() + +if(${CMAKE_BUILD_TYPE} MATCHES "Release") + target_compile_definitions(sfizz-vstgui PRIVATE "RELEASE") +endif() diff --git a/vst/external/VST_SDK/VST3_SDK/vstgui4 b/editor/external/vstgui4 similarity index 100% rename from vst/external/VST_SDK/VST3_SDK/vstgui4 rename to editor/external/vstgui4 diff --git a/editor/src/editor/EditIds.cpp b/editor/src/editor/EditIds.cpp new file mode 100644 index 00000000..1d148fb0 --- /dev/null +++ b/editor/src/editor/EditIds.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "EditIds.h" + +EditRange EditRange::get(EditId id) +{ + switch (id) { + default: + assert(false); + return {}; + case EditId::Volume: + return { 0, -60, 6 }; + case EditId::Polyphony: + return { 64, 1, 256 }; + case EditId::Oversampling: + return { 0, 0, 3 }; + case EditId::PreloadSize: + return { 8192, 1024, 65536 }; + case EditId::ScalaRootKey: + return { 60, 0, 127 }; + case EditId::TuningFrequency: + return { 440, 300, 500 }; + case EditId::StretchTuning: + return { 0, 0, 1 }; + case EditId::UIActivePanel: + return { 0, 0, 255 }; + } +} diff --git a/editor/src/editor/EditIds.h b/editor/src/editor/EditIds.h new file mode 100644 index 00000000..660853db --- /dev/null +++ b/editor/src/editor/EditIds.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include + +enum class EditId : int { + SfzFile, + Volume, + Polyphony, + Oversampling, + PreloadSize, + ScalaFile, + ScalaRootKey, + TuningFrequency, + StretchTuning, + UINumCurves, + UINumMasters, + UINumGroups, + UINumRegions, + UINumPreloadedSamples, + UINumActiveVoices, + UIActivePanel, +}; + +struct EditRange { + float def = 0.0; + float min = 0.0; + float max = 1.0; + constexpr EditRange() = default; + constexpr EditRange(float def, float min, float max) + : def(def), min(min), max(max) {} + static EditRange get(EditId id); +}; diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp new file mode 100644 index 00000000..c4d247c8 --- /dev/null +++ b/editor/src/editor/Editor.cpp @@ -0,0 +1,977 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Editor.h" +#include "EditorController.h" +#include "EditIds.h" +#include "GUIComponents.h" +#include +#include +#include + +#include "utility/vstgui_before.h" +#include "vstgui/vstgui.h" +#include "utility/vstgui_after.h" + +using namespace VSTGUI; + +const int Editor::viewWidth { 482 }; +const int Editor::viewHeight { 225 }; + +struct Editor::Impl : EditorController::Receiver, IControlListener { + EditorController* ctrl_ = nullptr; + CFrame* frame_ = nullptr; + SharedPointer view_; + + enum { + kPanelGeneral, + // kPanelControls, + kPanelSettings, + kPanelTuning, + kPanelInfo, + kNumPanels, + }; + + unsigned activePanel_ = 0; + CViewContainer* subPanels_[kNumPanels] = {}; + + enum { + kTagLoadSfzFile, + kTagSetVolume, + kTagSetNumVoices, + kTagSetOversampling, + kTagSetPreloadSize, + kTagLoadScalaFile, + kTagSetScalaRootKey, + kTagSetTuningFrequency, + kTagSetStretchedTuning, + kTagFirstChangePanel, + kTagLastChangePanel = kTagFirstChangePanel + kNumPanels - 1, + }; + + CTextLabel* sfzFileLabel_ = nullptr; + CTextLabel* scalaFileLabel_ = nullptr; + CSliderBase *volumeSlider_ = nullptr; + CTextLabel* volumeLabel_ = nullptr; + CSliderBase *numVoicesSlider_ = nullptr; + CTextLabel* numVoicesLabel_ = nullptr; + CSliderBase *oversamplingSlider_ = nullptr; + CTextLabel* oversamplingLabel_ = nullptr; + CSliderBase *preloadSizeSlider_ = nullptr; + CTextLabel* preloadSizeLabel_ = nullptr; + CSliderBase *scalaRootKeySlider_ = nullptr; + CTextLabel* scalaRootKeyLabel_ = nullptr; + CSliderBase *tuningFrequencySlider_ = nullptr; + CTextLabel* tuningFrequencyLabel_ = nullptr; + CSliderBase *stretchedTuningSlider_ = nullptr; + CTextLabel* stretchedTuningLabel_ = nullptr; + + CTextLabel* infoCurvesLabel_ = nullptr; + CTextLabel* infoMastersLabel_ = nullptr; + CTextLabel* infoGroupsLabel_ = nullptr; + CTextLabel* infoRegionsLabel_ = nullptr; + CTextLabel* infoSamplesLabel_ = nullptr; + CTextLabel* infoVoicesLabel_ = nullptr; + + void uiReceiveValue(EditId id, const EditValue& v) override; + + void createFrameContents(); + + template + void adjustMinMaxToEditRange(Control* c, EditId id) + { + const EditRange er = EditRange::get(id); + c->setMin(er.min); + c->setMax(er.max); + c->setDefaultValue(er.def); + } + + void chooseSfzFile(); + void chooseScalaFile(); + + void updateSfzFileLabel(const std::string& filePath); + void updateScalaFileLabel(const std::string& filePath); + static void updateLabelWithFileName(CTextLabel* label, const std::string& filePath); + void updateVolumeLabel(float volume); + void updateNumVoicesLabel(int numVoices); + void updateOversamplingLabel(int oversamplingLog2); + void updatePreloadSizeLabel(int preloadSize); + void updateScalaRootKeyLabel(int rootKey); + void updateTuningFrequencyLabel(float tuningFrequency); + void updateStretchedTuningLabel(float stretchedTuning); + + void setActivePanel(unsigned panelId); + + static void formatLabel(CTextLabel* label, const char* fmt, ...); + static void vformatLabel(CTextLabel* label, const char* fmt, va_list ap); + + // IControlListener + void valueChanged(CControl* ctl) override; + void enterOrLeaveEdit(CControl* ctl, bool enter); + void controlBeginEdit(CControl* ctl) override; + void controlEndEdit(CControl* ctl) override; +}; + +Editor::Editor(EditorController& ctrl) + : impl_(new Impl) +{ + Impl& impl = *impl_; + + impl.ctrl_ = &ctrl; + + ctrl.decorate(&impl); + + impl.createFrameContents(); +} + +Editor::~Editor() +{ + Impl& impl = *impl_; + + EditorController& ctrl = *impl.ctrl_; + ctrl.decorate(nullptr); +} + +void Editor::open(CFrame& frame) +{ + Impl& impl = *impl_; + + impl.frame_ = &frame; + frame.addView(impl.view_.get()); +} + +void Editor::close() +{ + Impl& impl = *impl_; + + if (impl.frame_) { + impl.frame_->removeView(impl.view_.get()); + impl.frame_ = nullptr; + } +} + +void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) +{ + switch (id) { + case EditId::SfzFile: + { + const std::string& value = absl::get(v); + updateSfzFileLabel(value); + } + break; + case EditId::Volume: + { + const float value = absl::get(v); + if (volumeSlider_) + volumeSlider_->setValue(value); + updateVolumeLabel(value); + } + break; + case EditId::Polyphony: + { + const int value = static_cast(absl::get(v)); + if (numVoicesSlider_) + numVoicesSlider_->setValue(value); + updateNumVoicesLabel(value); + } + break; + case EditId::Oversampling: + { + const int value = static_cast(absl::get(v)); + + int log2Value = 0; + for (int f = value; f > 1; f /= 2) + ++log2Value; + + if (oversamplingSlider_) + oversamplingSlider_->setValue(log2Value); + updateOversamplingLabel(log2Value); + } + break; + case EditId::PreloadSize: + { + const int value = static_cast(absl::get(v)); + if (preloadSizeSlider_) + preloadSizeSlider_->setValue(value); + updatePreloadSizeLabel(value); + } + break; + case EditId::ScalaFile: + { + const std::string& value = absl::get(v); + updateScalaFileLabel(value); + } + break; + case EditId::ScalaRootKey: + { + const int value = static_cast(absl::get(v)); + if (scalaRootKeySlider_) + scalaRootKeySlider_->setValue(value); + updateScalaRootKeyLabel(value); + } + break; + case EditId::TuningFrequency: + { + const float value = absl::get(v); + if (tuningFrequencySlider_) + tuningFrequencySlider_->setValue(value); + updateTuningFrequencyLabel(value); + } + break; + case EditId::StretchTuning: + { + const float value = absl::get(v); + if (stretchedTuningSlider_) + stretchedTuningSlider_->setValue(value); + updateStretchedTuningLabel(value); + } + break; + case EditId::UINumCurves: + { + const int value = static_cast(absl::get(v)); + if (CTextLabel* label = infoCurvesLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UINumMasters: + { + const int value = static_cast(absl::get(v)); + if (CTextLabel* label = infoMastersLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UINumGroups: + { + const int value = static_cast(absl::get(v)); + if (CTextLabel* label = infoGroupsLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UINumRegions: + { + const int value = static_cast(absl::get(v)); + if (CTextLabel* label = infoRegionsLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UINumPreloadedSamples: + { + const int value = static_cast(absl::get(v)); + if (CTextLabel* label = infoSamplesLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UINumActiveVoices: + { + const int value = static_cast(absl::get(v)); + if (CTextLabel* label = infoVoicesLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UIActivePanel: + { + const int value = static_cast(absl::get(v)); + setActivePanel(value); + } + break; + } +} + +void Editor::Impl::createFrameContents() +{ + const CRect bounds { 0.0, 0.0, viewWidth, viewHeight }; + CViewContainer* view = new CViewContainer(bounds); + view_ = view; + + view->setBackgroundColor(CColor(0xff, 0xff, 0xff)); + + SharedPointer logo = new CBitmap("logo.png"); + + CRect bottomRow = bounds; + bottomRow.top = bottomRow.bottom - 30; + + CRect topRow = bounds; + topRow.bottom = topRow.top + 30; + + CViewContainer* panel; + activePanel_ = 0; + + CRect topLeftLabelBox = topRow; + topLeftLabelBox.right -= 20 * kNumPanels; + + // general panel + { + panel = new CViewContainer(bounds); + view->addView(panel); + panel->setTransparency(true); + + CKickButton* sfizzButton = new CKickButton(bounds, this, kTagLoadSfzFile, logo); + panel->addView(sfizzButton); + + CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "No file loaded"); + topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); + topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + panel->addView(topLeftLabel); + sfzFileLabel_ = topLeftLabel; + + subPanels_[kPanelGeneral] = panel; + } + + // settings panel + { + panel = new CViewContainer(bounds); + view->addView(panel); + panel->setTransparency(true); + + CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Settings"); + topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); + topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + panel->addView(topLeftLabel); + + CRect row = topRow; + row.top += 45.0; + row.bottom += 45.0; + row.left += 20.0; + row.right -= 20.0; + + static const CCoord interRow = 35.0; + static const CCoord interColumn = 20.0; + static const int numColumns = 3; + + auto nthColumn = [&row](int colIndex) -> CRect { + CRect div = row; + CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; + div.left = div.left + colIndex * (columnWidth + interColumn); + div.right = div.left + columnWidth; + return div; + }; + + CTextLabel* label; + SimpleSlider* slider; + + label = new CTextLabel(nthColumn(0), "Volume"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + slider = new SimpleSlider(nthColumn(1), this, kTagSetVolume); + panel->addView(slider); + adjustMinMaxToEditRange(slider, EditId::Volume); + volumeSlider_ = slider; + label = new CTextLabel(nthColumn(2), ""); + volumeLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Polyphony"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + slider = new SimpleSlider(nthColumn(1), this, kTagSetNumVoices); + panel->addView(slider); + adjustMinMaxToEditRange(slider, EditId::Polyphony); + numVoicesSlider_ = slider; + label = new CTextLabel(nthColumn(2), ""); + numVoicesLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Oversampling"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + slider = new SimpleSlider(nthColumn(1), this, kTagSetOversampling); + panel->addView(slider); + adjustMinMaxToEditRange(slider, EditId::Oversampling); + oversamplingSlider_ = slider; + label = new CTextLabel(nthColumn(2), ""); + oversamplingLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Preload size"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + slider = new SimpleSlider(nthColumn(1), this, kTagSetPreloadSize); + panel->addView(slider); + adjustMinMaxToEditRange(slider, EditId::PreloadSize); + preloadSizeSlider_ = slider; + label = new CTextLabel(nthColumn(2), ""); + preloadSizeLabel_ = label; + panel->addView(label); + + subPanels_[kPanelSettings] = panel; + } + + // tuning panel + { + panel = new CViewContainer(bounds); + view->addView(panel); + panel->setTransparency(true); + + CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Tuning"); + topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); + topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + panel->addView(topLeftLabel); + + CRect row = topRow; + row.top += 45.0; + row.bottom += 45.0; + row.left += 20.0; + row.right -= 20.0; + + static const CCoord interRow = 35.0; + static const CCoord interColumn = 20.0; + static const int numColumns = 3; + + auto nthColumn = [&row](int colIndex) -> CRect { + CRect div = row; + CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; + div.left = div.left + colIndex * (columnWidth + interColumn); + div.right = div.left + columnWidth; + return div; + }; + + CTextLabel* label; + SimpleSlider* slider; + CTextButton* textbutton; + + label = new CTextLabel(nthColumn(0), "Scala file"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + textbutton = new CTextButton(nthColumn(1), this, kTagLoadScalaFile, "Choose"); + panel->addView(textbutton); + label = new CTextLabel(nthColumn(2), ""); + scalaFileLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Scala root key"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + slider = new SimpleSlider(nthColumn(1), this, kTagSetScalaRootKey); + panel->addView(slider); + adjustMinMaxToEditRange(slider, EditId::ScalaRootKey); + scalaRootKeySlider_ = slider; + label = new CTextLabel(nthColumn(2), ""); + scalaRootKeyLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Tuning frequency"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + slider = new SimpleSlider(nthColumn(1), this, kTagSetTuningFrequency); + panel->addView(slider); + adjustMinMaxToEditRange(slider, EditId::TuningFrequency); + tuningFrequencySlider_ = slider; + label = new CTextLabel(nthColumn(2), ""); + tuningFrequencyLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Stretched tuning"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + slider = new SimpleSlider(nthColumn(1), this, kTagSetStretchedTuning); + panel->addView(slider); + adjustMinMaxToEditRange(slider, EditId::StretchTuning); + stretchedTuningSlider_ = slider; + label = new CTextLabel(nthColumn(2), ""); + stretchedTuningLabel_ = label; + panel->addView(label); + + subPanels_[kPanelTuning] = panel; + } + + // info panel + { + panel = new CViewContainer(bounds); + view->addView(panel); + panel->setTransparency(true); + + CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Information"); + topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); + topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + panel->addView(topLeftLabel); + + CRect row = topRow; + row.top += 45.0; + row.bottom += 45.0; + row.left += 20.0; + row.right -= 20.0; + + static const CCoord interRow = 20.0; + static const CCoord interColumn = 20.0; + static const int numColumns = 3; + + auto nthColumn = [&row](int colIndex) -> CRect { + CRect div = row; + CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; + div.left = div.left + colIndex * (columnWidth + interColumn); + div.right = div.left + columnWidth; + return div; + }; + + CTextLabel* label; + + label = new CTextLabel(nthColumn(0), "Curves"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + infoCurvesLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Masters"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + infoMastersLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Groups"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + infoGroupsLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Regions"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + infoRegionsLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Samples"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + infoSamplesLabel_ = label; + panel->addView(label); + + row.top += interRow; + row.bottom += interRow; + + label = new CTextLabel(nthColumn(0), "Voices"); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + panel->addView(label); + label = new CTextLabel(nthColumn(1), ""); + label->setFontColor(CColor(0x00, 0x00, 0x00)); + label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + label->setHoriAlign(kLeftText); + infoVoicesLabel_ = label; + panel->addView(label); + + subPanels_[kPanelInfo] = panel; + } + + // all panels + for (unsigned currentPanel = 0; currentPanel < kNumPanels; ++currentPanel) { + panel = subPanels_[currentPanel]; + + CTextLabel* descLabel = new CTextLabel( + bottomRow, "Paul Ferrand and the SFZ Tools work group"); + descLabel->setFontColor(CColor(0x00, 0x00, 0x00)); + descLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + panel->addView(descLabel); + + for (unsigned i = 0; i < kNumPanels; ++i) { + CRect btnRect = topRow; + btnRect.left = topRow.right - (kNumPanels - i) * 50; + btnRect.right = btnRect.left + 50; + + const char *text; + switch (i) { + case kPanelGeneral: text = "File"; break; + case kPanelSettings: text = "Setup"; break; + case kPanelTuning: text = "Tuning"; break; + case kPanelInfo: text = "Info"; break; + default: text = "?"; break; + } + + CTextButton* changePanelButton = new CTextButton(btnRect, this, kTagFirstChangePanel + i, text); + panel->addView(changePanelButton); + + changePanelButton->setRoundRadius(0.0); + } + + panel->setVisible(currentPanel == activePanel_); + } +} + +void Editor::Impl::chooseSfzFile() +{ + SharedPointer fs(CNewFileSelector::create(frame_)); + + fs->setTitle("Load SFZ file"); + fs->setDefaultExtension(CFileExtension("SFZ", "sfz")); + + if (fs->runModal()) { + UTF8StringPtr file = fs->getSelectedFile(0); + if (file) { + std::string str(file); + ctrl_->uiSendValue(EditId::SfzFile, str); + updateSfzFileLabel(str); + } + } +} + +void Editor::Impl::chooseScalaFile() +{ + SharedPointer fs(CNewFileSelector::create(frame_)); + + fs->setTitle("Load Scala file"); + fs->setDefaultExtension(CFileExtension("SCL", "scl")); + + if (fs->runModal()) { + UTF8StringPtr file = fs->getSelectedFile(0); + if (file) { + std::string str(file); + ctrl_->uiSendValue(EditId::ScalaFile, str); + updateScalaFileLabel(str); + } + } +} + +void Editor::Impl::updateSfzFileLabel(const std::string& filePath) +{ + updateLabelWithFileName(sfzFileLabel_, filePath); +} + +void Editor::Impl::updateScalaFileLabel(const std::string& filePath) +{ + updateLabelWithFileName(scalaFileLabel_, filePath); +} + +void Editor::Impl::updateLabelWithFileName(CTextLabel* label, const std::string& filePath) +{ + if (!label) + return; + + std::string fileName; + if (filePath.empty()) + fileName = ""; + else { +#if defined (_WIN32) + size_t pos = filePath.find_last_of("/\\"); +#else + size_t pos = filePath.rfind('/'); +#endif + fileName = (pos != filePath.npos) ? + filePath.substr(pos + 1) : filePath; + } + label->setText(fileName.c_str()); +} + +void Editor::Impl::updateVolumeLabel(float volume) +{ + CTextLabel* label = volumeLabel_; + if (!label) + return; + + char text[64]; + sprintf(text, "%.1f dB", volume); + text[sizeof(text) - 1] = '\0'; + label->setText(text); +} + +void Editor::Impl::updateNumVoicesLabel(int numVoices) +{ + CTextLabel* label = numVoicesLabel_; + if (!label) + return; + + char text[64]; + sprintf(text, "%d", numVoices); + text[sizeof(text) - 1] = '\0'; + label->setText(text); +} + +void Editor::Impl::updateOversamplingLabel(int oversamplingLog2) +{ + CTextLabel* label = oversamplingLabel_; + if (!label) + return; + + char text[64]; + sprintf(text, "%dx", 1 << oversamplingLog2); + text[sizeof(text) - 1] = '\0'; + label->setText(text); +} + +void Editor::Impl::updatePreloadSizeLabel(int preloadSize) +{ + CTextLabel* label = preloadSizeLabel_; + if (!label) + return; + + char text[64]; + sprintf(text, "%.1f kB", preloadSize * (1.0 / 1024)); + text[sizeof(text) - 1] = '\0'; + label->setText(text); +} + +void Editor::Impl::updateScalaRootKeyLabel(int rootKey) +{ + CTextLabel* label = scalaRootKeyLabel_; + if (!label) + return; + + static const char *octNoteNames[12] = { + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", + }; + + auto noteName = [](int key) -> std::string + { + int octNum; + int octNoteNum; + if (key >= 0) { + octNum = key / 12 - 1; + octNoteNum = key % 12; + } + else { + octNum = -2 - (key + 1) / -12; + octNoteNum = (key % 12 + 12) % 12; + } + return std::string(octNoteNames[octNoteNum]) + std::to_string(octNum); + }; + + label->setText(noteName(rootKey)); +} + +void Editor::Impl::updateTuningFrequencyLabel(float tuningFrequency) +{ + CTextLabel* label = tuningFrequencyLabel_; + if (!label) + return; + + char text[64]; + sprintf(text, "%.1f", tuningFrequency); + text[sizeof(text) - 1] = '\0'; + label->setText(text); +} + +void Editor::Impl::updateStretchedTuningLabel(float stretchedTuning) +{ + CTextLabel* label = stretchedTuningLabel_; + if (!label) + return; + + char text[64]; + sprintf(text, "%.3f", stretchedTuning); + text[sizeof(text) - 1] = '\0'; + label->setText(text); +} + +void Editor::Impl::setActivePanel(unsigned panelId) +{ + panelId = std::max(0, std::min(kNumPanels - 1, static_cast(panelId))); + + if (activePanel_ != panelId) { + subPanels_[activePanel_]->setVisible(false); + activePanel_ = panelId; + subPanels_[panelId]->setVisible(true); + } +} + +void Editor::Impl::formatLabel(CTextLabel* label, const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vformatLabel(label, fmt, ap); + va_end(ap); +} + +void Editor::Impl::vformatLabel(CTextLabel* label, const char* fmt, va_list ap) +{ + char text[256]; + vsprintf(text, fmt, ap); + text[sizeof(text) - 1] = '\0'; + label->setText(text); +} + +void Editor::Impl::valueChanged(CControl* ctl) +{ + int32_t tag = ctl->getTag(); + float value = ctl->getValue(); + EditorController& ctrl = *ctrl_; + + switch (tag) { + case kTagLoadSfzFile: + if (value != 1) + break; + + Call::later([this]() { chooseSfzFile(); }); + break; + + case kTagLoadScalaFile: + if (value != 1) + break; + + Call::later([this]() { chooseScalaFile(); }); + break; + + case kTagSetVolume: + ctrl.uiSendValue(EditId::Volume, value); + updateVolumeLabel(value); + break; + + case kTagSetNumVoices: + ctrl.uiSendValue(EditId::Polyphony, value); + updateNumVoicesLabel(static_cast(value)); + break; + + case kTagSetOversampling: + ctrl.uiSendValue(EditId::Oversampling, 1 << static_cast(value)); + updateOversamplingLabel(static_cast(value)); + break; + + case kTagSetPreloadSize: + ctrl.uiSendValue(EditId::PreloadSize, value); + updatePreloadSizeLabel(static_cast(value)); + break; + + case kTagSetScalaRootKey: + ctrl.uiSendValue(EditId::ScalaRootKey, value); + updateScalaRootKeyLabel(static_cast(value)); + break; + + case kTagSetTuningFrequency: + ctrl.uiSendValue(EditId::TuningFrequency, value); + updateTuningFrequencyLabel(value); + break; + + case kTagSetStretchedTuning: + ctrl.uiSendValue(EditId::StretchTuning, value); + updateStretchedTuningLabel(value); + break; + + default: + if (tag >= kTagFirstChangePanel && tag <= kTagLastChangePanel) { + int panelId = tag - kTagFirstChangePanel; + ctrl.uiSendValue(EditId::UIActivePanel, panelId); + setActivePanel(panelId); + } + break; + } +} + +void Editor::Impl::enterOrLeaveEdit(CControl* ctl, bool enter) +{ + int32_t tag = ctl->getTag(); + EditId id; + + switch (tag) { + case kTagSetVolume: id = EditId::Volume; break; + case kTagSetNumVoices: id = EditId::Polyphony; break; + case kTagSetOversampling: id = EditId::Oversampling; break; + case kTagSetPreloadSize: id = EditId::PreloadSize; break; + case kTagSetScalaRootKey: id = EditId::ScalaRootKey; break; + case kTagSetTuningFrequency: id = EditId::TuningFrequency; break; + case kTagSetStretchedTuning: id = EditId::StretchTuning; break; + default: return; + } + + EditorController& ctrl = *ctrl_; + if (enter) + ctrl.uiBeginSend(id); + else + ctrl.uiEndSend(id); +} + +void Editor::Impl::controlBeginEdit(CControl* ctl) +{ + enterOrLeaveEdit(ctl, true); +} + +void Editor::Impl::controlEndEdit(CControl* ctl) +{ + enterOrLeaveEdit(ctl, false); +} diff --git a/editor/src/editor/Editor.h b/editor/src/editor/Editor.h new file mode 100644 index 00000000..0c0ae1de --- /dev/null +++ b/editor/src/editor/Editor.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +class EditorController; + +#include "utility/vstgui_before.h" +#include "vstgui/lib/vstguifwd.h" +#include "utility/vstgui_after.h" +using VSTGUI::CFrame; + +class Editor { +public: + static const int viewWidth; + static const int viewHeight; + + explicit Editor(EditorController& ctrl); + ~Editor(); + + void open(CFrame& frame); + void close(); + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/editor/src/editor/EditorController.h b/editor/src/editor/EditorController.h new file mode 100644 index 00000000..6b6988c1 --- /dev/null +++ b/editor/src/editor/EditorController.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include +#include +enum class EditId : int; +typedef absl::variant EditValue; + +class EditorController { +public: + virtual ~EditorController() {} + + // called by Editor + virtual void uiSendValue(EditId id, const EditValue& v) = 0; + virtual void uiBeginSend(EditId id) = 0; + virtual void uiEndSend(EditId id) = 0; + virtual void uiSendMIDI(const uint8_t* msg, uint32_t len) = 0; + class Receiver; + void decorate(Receiver* r) { r_ = r; } + + class Receiver { + public: + virtual ~Receiver() {} + virtual void uiReceiveValue(EditId id, const EditValue& v) = 0; + }; + + // called by DSP + void uiReceiveValue(EditId id, const EditValue& v); + +private: + Receiver* r_ = nullptr; +}; + +inline void EditorController::uiReceiveValue(EditId id, const EditValue& v) +{ + if (r_) + r_->uiReceiveValue(id, v); +} diff --git a/vst/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp similarity index 93% rename from vst/GUIComponents.cpp rename to editor/src/editor/GUIComponents.cpp index 314ba642..56fb6f8e 100644 --- a/vst/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -5,7 +5,10 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "GUIComponents.h" + +#include "utility/vstgui_before.h" #include "vstgui/lib/cdrawcontext.h" +#include "utility/vstgui_after.h" SimpleSlider::SimpleSlider(const CRect& bounds, IControlListener* listener, int32_t tag) : CSliderBase(bounds, listener, tag) diff --git a/vst/GUIComponents.h b/editor/src/editor/GUIComponents.h similarity index 91% rename from vst/GUIComponents.h rename to editor/src/editor/GUIComponents.h index 003d219b..c76c2e44 100644 --- a/vst/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -5,8 +5,11 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once + +#include "utility/vstgui_before.h" #include "vstgui/lib/controls/cslider.h" #include "vstgui/lib/ccolor.h" +#include "utility/vstgui_after.h" using namespace VSTGUI; diff --git a/editor/src/editor/utility/vstgui_after.h b/editor/src/editor/utility/vstgui_after.h new file mode 100644 index 00000000..69e3b4db --- /dev/null +++ b/editor/src/editor/utility/vstgui_after.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif diff --git a/editor/src/editor/utility/vstgui_before.h b/editor/src/editor/utility/vstgui_before.h new file mode 100644 index 00000000..bb01d0c3 --- /dev/null +++ b/editor/src/editor/utility/vstgui_before.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wignored-qualifiers" +#pragma GCC diagnostic ignored "-Wdeprecated-copy" +#endif diff --git a/lv2/CMakeLists.txt b/lv2/CMakeLists.txt index 73de6ee5..4104eb25 100644 --- a/lv2/CMakeLists.txt +++ b/lv2/CMakeLists.txt @@ -45,7 +45,7 @@ file (MAKE_DIRECTORY ${PROJECT_BINARY_DIR}) configure_file (manifest.ttl.in ${PROJECT_BINARY_DIR}/manifest.ttl) configure_file (${PROJECT_NAME}.ttl.in ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.ttl) configure_file (LICENSE.md.in ${PROJECT_BINARY_DIR}/LICENSE.md) -if (SFIZZ_USE_VCPKG OR SFIZZ_STATIC_LIBSNDFILE OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") +if (SFIZZ_USE_VCPKG OR SFIZZ_STATIC_DEPENDENCIES OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") file(COPY "lgpl-3.0.txt" DESTINATION ${PROJECT_BINARY_DIR}) endif() diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index 0c14922d..5d0301e6 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -17,7 +17,6 @@ set(VSTPLUGIN_SOURCES SfizzVstController.cpp SfizzVstEditor.cpp SfizzVstState.cpp - GUIComponents.cpp VstPluginFactory.cpp X11RunLoop.cpp) @@ -26,7 +25,6 @@ set(VSTPLUGIN_HEADERS SfizzVstController.h SfizzVstEditor.h SfizzVstState.h - GUIComponents.h X11RunLoop.h) set(VSTPLUGIN_RESOURCES @@ -40,7 +38,8 @@ if(WIN32) target_sources(${VSTPLUGIN_PRJ_NAME} PRIVATE vst3.def) endif() target_link_libraries(${VSTPLUGIN_PRJ_NAME} - PRIVATE ${PROJECT_NAME}::${PROJECT_NAME}) + PRIVATE ${PROJECT_NAME}::${PROJECT_NAME} + PRIVATE sfizz_editor) target_include_directories(${VSTPLUGIN_PRJ_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") set_target_properties(${VSTPLUGIN_PRJ_NAME} PROPERTIES diff --git a/vst/SfizzVstController.cpp b/vst/SfizzVstController.cpp index 8f13bab2..35eba7a0 100644 --- a/vst/SfizzVstController.cpp +++ b/vst/SfizzVstController.cpp @@ -35,15 +35,15 @@ tresult PLUGIN_API SfizzVstControllerNoUi::initialize(FUnknown* context) Steinberg::String("Preload size"), pid++, nullptr, 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); parameters.addParameter( - kParamScalaRootKey.createParameter( + kParamScalaRootKeyRange.createParameter( Steinberg::String("Scala root key"), pid++, nullptr, 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); parameters.addParameter( - kParamTuningFrequency.createParameter( + kParamTuningFrequencyRange.createParameter( Steinberg::String("Tuning frequency"), pid++, Steinberg::String("Hz"), 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); parameters.addParameter( - kParamStretchedTuning.createParameter( + kParamStretchedTuningRange.createParameter( Steinberg::String("Stretched tuning"), pid++, nullptr, 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); @@ -175,17 +175,17 @@ tresult PLUGIN_API SfizzVstController::setParamNormalized(Vst::ParamID tag, Vst: } case kPidScalaRootKey: { slotI32 = &_state.scalaRootKey; - value = kParamScalaRootKey.denormalize(normValue); + value = kParamScalaRootKeyRange.denormalize(normValue); break; } case kPidTuningFrequency: { slotF32 = &_state.tuningFrequency; - value = kParamTuningFrequency.denormalize(normValue); + value = kParamTuningFrequencyRange.denormalize(normValue); break; } case kPidStretchedTuning: { slotF32 = &_state.stretchedTuning; - value = kParamStretchedTuning.denormalize(normValue); + value = kParamStretchedTuningRange.denormalize(normValue); break; } } @@ -244,9 +244,9 @@ tresult PLUGIN_API SfizzVstController::setComponentState(IBStream* state) setParamNormalized(kPidNumVoices, kParamNumVoicesRange.normalize(s.numVoices)); setParamNormalized(kPidOversampling, kParamOversamplingRange.normalize(s.oversamplingLog2)); setParamNormalized(kPidPreloadSize, kParamPreloadSizeRange.normalize(s.preloadSize)); - setParamNormalized(kPidScalaRootKey, kParamScalaRootKey.normalize(s.scalaRootKey)); - setParamNormalized(kPidTuningFrequency, kParamTuningFrequency.normalize(s.tuningFrequency)); - setParamNormalized(kPidStretchedTuning, kParamStretchedTuning.normalize(s.stretchedTuning)); + setParamNormalized(kPidScalaRootKey, kParamScalaRootKeyRange.normalize(s.scalaRootKey)); + setParamNormalized(kPidTuningFrequency, kParamTuningFrequencyRange.normalize(s.tuningFrequency)); + setParamNormalized(kPidStretchedTuning, kParamStretchedTuningRange.normalize(s.stretchedTuning)); for (StateListener* listener : _stateListeners) listener->onStateChanged(); diff --git a/vst/SfizzVstEditor.cpp b/vst/SfizzVstEditor.cpp index 5863cdc5..163034cc 100644 --- a/vst/SfizzVstEditor.cpp +++ b/vst/SfizzVstEditor.cpp @@ -6,18 +6,18 @@ #include "SfizzVstEditor.h" #include "SfizzVstState.h" -#include "GUIComponents.h" +#include "editor/Editor.h" +#include "editor/EditIds.h" #if !defined(__APPLE__) && !defined(_WIN32) #include "X11RunLoop.h" #endif using namespace VSTGUI; -static ViewRect sfizzUiViewRect {0, 0, 482, 225}; +static ViewRect sfizzUiViewRect { 0, 0, Editor::viewWidth, Editor::viewHeight }; SfizzVstEditor::SfizzVstEditor(void *controller) - : VSTGUIEditor(controller, &sfizzUiViewRect), - _logo("logo.png") + : VSTGUIEditor(controller, &sfizzUiViewRect) { getController()->addSfizzStateListener(this); } @@ -45,7 +45,11 @@ bool PLUGIN_API SfizzVstEditor::open(void* parent, const VSTGUI::PlatformType& p config = &x11config; #endif - createFrameContents(); + Editor* editor = editor_.get(); + if (!editor) { + editor = new Editor(*this); + editor_.reset(editor); + } updateStateDisplay(); if (!frame->open(parent, platformType, config)) { @@ -53,6 +57,8 @@ bool PLUGIN_API SfizzVstEditor::open(void* parent, const VSTGUI::PlatformType& p return false; } + editor->open(*frame); + return true; } @@ -60,121 +66,16 @@ void PLUGIN_API SfizzVstEditor::close() { CFrame *frame = this->frame; if (frame) { - frame->removeAll(); + if (editor_) + editor_->close(); if (frame->getNbReference() != 1) - frame->forget (); - else { + frame->forget(); + else frame->close(); - this->frame = nullptr; - } } } /// -void SfizzVstEditor::valueChanged(CControl* ctl) -{ - int32_t tag = ctl->getTag(); - float value = ctl->getValue(); - float valueNorm = ctl->getValueNormalized(); - SfizzVstController* controller = getController(); - - switch (tag) { - case kTagLoadSfzFile: - if (value != 1) - break; - - Call::later([this]() { chooseSfzFile(); }); - break; - - case kTagLoadScalaFile: - if (value != 1) - break; - - Call::later([this]() { chooseScalaFile(); }); - break; - - case kTagSetVolume: - controller->setParamNormalized(kPidVolume, valueNorm); - controller->performEdit(kPidVolume, valueNorm); - updateVolumeLabel(value); - break; - - case kTagSetNumVoices: - controller->setParamNormalized(kPidNumVoices, valueNorm); - controller->performEdit(kPidNumVoices, valueNorm); - updateNumVoicesLabel(static_cast(value)); - break; - - case kTagSetOversampling: - controller->setParamNormalized(kPidOversampling, valueNorm); - controller->performEdit(kPidOversampling, valueNorm); - updateOversamplingLabel(static_cast(value)); - break; - - case kTagSetPreloadSize: - controller->setParamNormalized(kPidPreloadSize, valueNorm); - controller->performEdit(kPidPreloadSize, valueNorm); - updatePreloadSizeLabel(static_cast(value)); - break; - - case kTagSetScalaRootKey: - controller->setParamNormalized(kPidScalaRootKey, valueNorm); - controller->performEdit(kPidScalaRootKey, valueNorm); - updateScalaRootKeyLabel(static_cast(value)); - break; - - case kTagSetTuningFrequency: - controller->setParamNormalized(kPidTuningFrequency, valueNorm); - controller->performEdit(kPidTuningFrequency, valueNorm); - updateTuningFrequencyLabel(value); - break; - - case kTagSetStretchedTuning: - controller->setParamNormalized(kPidStretchedTuning, valueNorm); - controller->performEdit(kPidStretchedTuning, valueNorm); - updateStretchedTuningLabel(value); - break; - - default: - if (tag >= kTagFirstChangePanel && tag <= kTagLastChangePanel) - setActivePanel(tag - kTagFirstChangePanel); - break; - } -} - -void SfizzVstEditor::enterOrLeaveEdit(CControl* ctl, bool enter) -{ - int32_t tag = ctl->getTag(); - Vst::ParamID id; - - switch (tag) { - case kTagSetVolume: id = kPidVolume; break; - case kTagSetNumVoices: id = kPidNumVoices; break; - case kTagSetOversampling: id = kPidOversampling; break; - case kTagSetPreloadSize: id = kPidPreloadSize; break; - case kTagSetScalaRootKey: id = kPidScalaRootKey; break; - case kTagSetTuningFrequency: id = kPidTuningFrequency; break; - case kTagSetStretchedTuning: id = kPidStretchedTuning; break; - default: return; - } - - SfizzVstController* controller = getController(); - if (enter) - controller->beginEdit(id); - else - controller->endEdit(id); -} - -void SfizzVstEditor::controlBeginEdit(CControl* ctl) -{ - enterOrLeaveEdit(ctl, true); -} - -void SfizzVstEditor::controlEndEdit(CControl* ctl) -{ - enterOrLeaveEdit(ctl, false); -} - CMessageResult SfizzVstEditor::notify(CBaseObject* sender, const char* message) { CMessageResult result = VSTGUIEditor::notify(sender, message); @@ -205,20 +106,83 @@ void SfizzVstEditor::onStateChanged() } /// -void SfizzVstEditor::chooseSfzFile() +void SfizzVstEditor::uiSendValue(EditId id, const EditValue& v) { - SharedPointer fs(CNewFileSelector::create(frame)); + if (id == EditId::SfzFile) + loadSfzFile(absl::get(v)); + else if (id == EditId::ScalaFile) + loadScalaFile(absl::get(v)); + else { + SfizzVstController* ctrl = getController(); - fs->setTitle("Load SFZ file"); - fs->setDefaultExtension(CFileExtension("SFZ", "sfz")); + auto normalizeAndSet = [ctrl](Vst::ParamID pid, const SfizzParameterRange& range, float value) { + float normValue = range.normalize(value); + ctrl->setParamNormalized(pid, normValue); + ctrl->performEdit(pid, normValue); + }; - if (fs->runModal()) { - UTF8StringPtr file = fs->getSelectedFile(0); - if (file) - loadSfzFile(file); + switch (id) { + case EditId::Volume: + normalizeAndSet(kPidVolume, kParamVolumeRange, absl::get(v)); + break; + case EditId::Polyphony: + normalizeAndSet(kPidNumVoices, kParamNumVoicesRange, absl::get(v)); + break; + case EditId::Oversampling: + { + const int32 value = static_cast(absl::get(v)); + + int32 log2Value = 0; + for (int32 f = value; f > 1; f /= 2) + ++log2Value; + + normalizeAndSet(kPidOversampling, kParamOversamplingRange, log2Value); + } + break; + case EditId::PreloadSize: + normalizeAndSet(kPidPreloadSize, kParamPreloadSizeRange, absl::get(v)); + break; + case EditId::ScalaRootKey: + normalizeAndSet(kPidScalaRootKey, kParamScalaRootKeyRange, absl::get(v)); + break; + case EditId::TuningFrequency: + normalizeAndSet(kPidTuningFrequency, kParamTuningFrequencyRange, absl::get(v)); + break; + case EditId::StretchTuning: + normalizeAndSet(kPidStretchedTuning, kParamStretchedTuningRange, absl::get(v)); + break; + + case EditId::UIActivePanel: + ctrl->getSfizzUiState().activePanel = static_cast(absl::get(v)); + break; + + default: + break; + } } } +void SfizzVstEditor::uiBeginSend(EditId id) +{ + Vst::ParamID pid = parameterOfEditId(id); + if (pid != -1) + getController()->beginEdit(pid); +} + +void SfizzVstEditor::uiEndSend(EditId id) +{ + Vst::ParamID pid = parameterOfEditId(id); + if (pid != -1) + getController()->endEdit(pid); +} + +void SfizzVstEditor::uiSendMIDI(const uint8_t* msg, uint32_t len) +{ + // TODO send MIDI... + +} + +/// void SfizzVstEditor::loadSfzFile(const std::string& filePath) { SfizzVstController* ctl = getController(); @@ -233,22 +197,6 @@ void SfizzVstEditor::loadSfzFile(const std::string& filePath) Vst::IAttributeList* attr = msg->getAttributes(); attr->setBinary("File", filePath.data(), filePath.size()); ctl->sendMessage(msg); - - updateSfzFileLabel(filePath); -} - -void SfizzVstEditor::chooseScalaFile() -{ - SharedPointer fs(CNewFileSelector::create(frame)); - - fs->setTitle("Load Scala file"); - fs->setDefaultExtension(CFileExtension("SCL", "scl")); - - if (fs->runModal()) { - UTF8StringPtr file = fs->getSelectedFile(0); - if (file) - loadScalaFile(file); - } } void SfizzVstEditor::loadScalaFile(const std::string& filePath) @@ -265,414 +213,6 @@ void SfizzVstEditor::loadScalaFile(const std::string& filePath) Vst::IAttributeList* attr = msg->getAttributes(); attr->setBinary("File", filePath.data(), filePath.size()); ctl->sendMessage(msg); - - updateScalaFileLabel(filePath); -} - -void SfizzVstEditor::createFrameContents() -{ - SfizzVstController* controller = getController(); - const SfizzUiState& uiState = controller->getSfizzUiState(); - - CFrame* frame = this->frame; - CRect bounds = frame->getViewSize(); - - frame->setBackgroundColor(CColor(0xff, 0xff, 0xff)); - - CRect bottomRow = bounds; - bottomRow.top = bottomRow.bottom - 30; - - CRect topRow = bounds; - topRow.bottom = topRow.top + 30; - - CViewContainer* panel; - _activePanel = std::max(0, std::min(kNumPanels - 1, static_cast(uiState.activePanel))); - - CRect topLeftLabelBox = topRow; - topLeftLabelBox.right -= 20 * kNumPanels; - - // general panel - { - panel = new CViewContainer(bounds); - frame->addView(panel); - panel->setTransparency(true); - - CKickButton* sfizzButton = new CKickButton(bounds, this, kTagLoadSfzFile, &_logo); - panel->addView(sfizzButton); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "No file loaded"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - _sfzFileLabel = topLeftLabel; - - _subPanels[kPanelGeneral] = panel; - } - - // settings panel - { - panel = new CViewContainer(bounds); - frame->addView(panel); - panel->setTransparency(true); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Settings"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - - CRect row = topRow; - row.top += 45.0; - row.bottom += 45.0; - row.left += 20.0; - row.right -= 20.0; - - static const CCoord interRow = 35.0; - static const CCoord interColumn = 20.0; - static const int numColumns = 3; - - auto nthColumn = [&row](int colIndex) -> CRect { - CRect div = row; - CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; - div.left = div.left + colIndex * (columnWidth + interColumn); - div.right = div.left + columnWidth; - return div; - }; - - CTextLabel* label; - SimpleSlider* slider; - - label = new CTextLabel(nthColumn(0), "Volume"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetVolume); - panel->addView(slider); - adjustMinMaxToRangeParam(slider, kPidVolume); - _volumeSlider = slider; - label = new CTextLabel(nthColumn(2), ""); - _volumeLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Polyphony"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetNumVoices); - panel->addView(slider); - adjustMinMaxToRangeParam(slider, kPidNumVoices); - _numVoicesSlider = slider; - label = new CTextLabel(nthColumn(2), ""); - _numVoicesLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Oversampling"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetOversampling); - panel->addView(slider); - adjustMinMaxToRangeParam(slider, kPidOversampling); - _oversamplingSlider = slider; - label = new CTextLabel(nthColumn(2), ""); - _oversamplingLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Preload size"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetPreloadSize); - panel->addView(slider); - adjustMinMaxToRangeParam(slider, kPidPreloadSize); - _preloadSizeSlider = slider; - label = new CTextLabel(nthColumn(2), ""); - _preloadSizeLabel = label; - panel->addView(label); - - _subPanels[kPanelSettings] = panel; - } - - // tuning panel - { - panel = new CViewContainer(bounds); - frame->addView(panel); - panel->setTransparency(true); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Tuning"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - - CRect row = topRow; - row.top += 45.0; - row.bottom += 45.0; - row.left += 20.0; - row.right -= 20.0; - - static const CCoord interRow = 35.0; - static const CCoord interColumn = 20.0; - static const int numColumns = 3; - - auto nthColumn = [&row](int colIndex) -> CRect { - CRect div = row; - CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; - div.left = div.left + colIndex * (columnWidth + interColumn); - div.right = div.left + columnWidth; - return div; - }; - - CTextLabel* label; - SimpleSlider* slider; - CTextButton* textbutton; - - label = new CTextLabel(nthColumn(0), "Scala file"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - textbutton = new CTextButton(nthColumn(1), this, kTagLoadScalaFile, "Choose"); - panel->addView(textbutton); - label = new CTextLabel(nthColumn(2), ""); - _scalaFileLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Scala root key"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetScalaRootKey); - panel->addView(slider); - adjustMinMaxToRangeParam(slider, kPidScalaRootKey); - _scalaRootKeySlider = slider; - label = new CTextLabel(nthColumn(2), ""); - _scalaRootKeyLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Tuning frequency"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetTuningFrequency); - panel->addView(slider); - adjustMinMaxToRangeParam(slider, kPidTuningFrequency); - _tuningFrequencySlider = slider; - label = new CTextLabel(nthColumn(2), ""); - _tuningFrequencyLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Stretched tuning"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetStretchedTuning); - panel->addView(slider); - adjustMinMaxToRangeParam(slider, kPidStretchedTuning); - _stretchedTuningSlider = slider; - label = new CTextLabel(nthColumn(2), ""); - _stretchedTuningLabel = label; - panel->addView(label); - - _subPanels[kPanelTuning] = panel; - } - - // info panel - { - panel = new CViewContainer(bounds); - frame->addView(panel); - panel->setTransparency(true); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Information"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - - CRect row = topRow; - row.top += 45.0; - row.bottom += 45.0; - row.left += 20.0; - row.right -= 20.0; - - static const CCoord interRow = 20.0; - static const CCoord interColumn = 20.0; - static const int numColumns = 3; - - auto nthColumn = [&row](int colIndex) -> CRect { - CRect div = row; - CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; - div.left = div.left + colIndex * (columnWidth + interColumn); - div.right = div.left + columnWidth; - return div; - }; - - CTextLabel* label; - - label = new CTextLabel(nthColumn(0), "Curves"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - _infoCurvesLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Masters"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - _infoMastersLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Groups"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - _infoGroupsLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Regions"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - _infoRegionsLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Samples"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - _infoSamplesLabel = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Voices"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - _infoVoicesLabel = label; - panel->addView(label); - - _subPanels[kPanelInfo] = panel; - } - - // all panels - for (unsigned currentPanel = 0; currentPanel < kNumPanels; ++currentPanel) { - panel = _subPanels[currentPanel]; - - CTextLabel* descLabel = new CTextLabel( - bottomRow, "Paul Ferrand and the SFZ Tools work group"); - descLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - descLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(descLabel); - - for (unsigned i = 0; i < kNumPanels; ++i) { - CRect btnRect = topRow; - btnRect.left = topRow.right - (kNumPanels - i) * 50; - btnRect.right = btnRect.left + 50; - - const char *text; - switch (i) { - case kPanelGeneral: text = "File"; break; - case kPanelSettings: text = "Setup"; break; - case kPanelTuning: text = "Tuning"; break; - case kPanelInfo: text = "Info"; break; - default: text = "?"; break; - } - - CTextButton* changePanelButton = new CTextButton(btnRect, this, kTagFirstChangePanel + i, text); - panel->addView(changePanelButton); - - changePanelButton->setRoundRadius(0.0); - } - - panel->setVisible(currentPanel == _activePanel); - } } void SfizzVstEditor::updateStateDisplay() @@ -686,196 +226,38 @@ void SfizzVstEditor::updateStateDisplay() const SfizzPlayState& playState = controller->getSfizzPlayState(); /// - updateSfzFileLabel(state.sfzFile); - if (_volumeSlider) - _volumeSlider->setValue(state.volume); - updateVolumeLabel(state.volume); - if (_numVoicesSlider) - _numVoicesSlider->setValue(state.numVoices); - updateNumVoicesLabel(state.numVoices); - if (_oversamplingSlider) - _oversamplingSlider->setValue(state.oversamplingLog2); - updateOversamplingLabel(state.oversamplingLog2); - if (_preloadSizeSlider) - _preloadSizeSlider->setValue(state.preloadSize); - updatePreloadSizeLabel(state.preloadSize); - updateScalaFileLabel(state.scalaFile); - if (_scalaRootKeySlider) - _scalaRootKeySlider->setValue(state.scalaRootKey); - updateScalaRootKeyLabel(state.scalaRootKey); - if (_tuningFrequencySlider) - _tuningFrequencySlider->setValue(state.tuningFrequency); - updateTuningFrequencyLabel(state.tuningFrequency); - if (_stretchedTuningSlider) - _stretchedTuningSlider->setValue(state.stretchedTuning); - updateStretchedTuningLabel(state.stretchedTuning); + uiReceiveValue(EditId::SfzFile, state.sfzFile); + uiReceiveValue(EditId::Volume, state.volume); + uiReceiveValue(EditId::Polyphony, state.numVoices); + uiReceiveValue(EditId::Oversampling, 1u << state.oversamplingLog2); + uiReceiveValue(EditId::PreloadSize, state.preloadSize); + uiReceiveValue(EditId::ScalaFile, state.scalaFile); + uiReceiveValue(EditId::ScalaRootKey, state.scalaRootKey); + uiReceiveValue(EditId::TuningFrequency, state.tuningFrequency); + uiReceiveValue(EditId::StretchTuning, state.stretchedTuning); /// - struct InfoLabel { const uint32* src; CTextLabel* dst; }; - for (const auto& item : { - InfoLabel{&playState.curves, _infoCurvesLabel}, - InfoLabel{&playState.masters, _infoMastersLabel}, - InfoLabel{&playState.groups, _infoGroupsLabel}, - InfoLabel{&playState.regions, _infoRegionsLabel}, - InfoLabel{&playState.preloadedSamples, _infoSamplesLabel}, - InfoLabel{&playState.activeVoices, _infoVoicesLabel} }) - { - if (item.dst) { - char text[64]; - sprintf(text, "%u", *item.src); - text[sizeof(text) - 1] = '\0'; - item.dst->setText(text); - } - } + uiReceiveValue(EditId::UINumCurves, playState.curves); + uiReceiveValue(EditId::UINumMasters, playState.masters); + uiReceiveValue(EditId::UINumGroups, playState.groups); + uiReceiveValue(EditId::UINumRegions, playState.regions); + uiReceiveValue(EditId::UINumPreloadedSamples, playState.preloadedSamples); + uiReceiveValue(EditId::UINumActiveVoices, playState.activeVoices); /// - setActivePanel(uiState.activePanel); + uiReceiveValue(EditId::UIActivePanel, uiState.activePanel); } -void SfizzVstEditor::updateSfzFileLabel(const std::string& filePath) +Vst::ParamID SfizzVstEditor::parameterOfEditId(EditId id) { - updateLabelWithFileName(_sfzFileLabel, filePath); -} - -void SfizzVstEditor::updateScalaFileLabel(const std::string& filePath) -{ - updateLabelWithFileName(_scalaFileLabel, filePath); -} - -void SfizzVstEditor::updateLabelWithFileName(CTextLabel* label, const std::string& filePath) -{ - if (!label) - return; - - std::string fileName; - if (filePath.empty()) - fileName = ""; - else { -#if defined (_WIN32) - size_t pos = filePath.find_last_of("/\\"); -#else - size_t pos = filePath.rfind('/'); -#endif - fileName = (pos != filePath.npos) ? - filePath.substr(pos + 1) : filePath; - } - label->setText(fileName.c_str()); -} - -void SfizzVstEditor::updateVolumeLabel(float volume) -{ - CTextLabel* label = _volumeLabel; - if (!label) - return; - - char text[64]; - sprintf(text, "%.1f dB", volume); - text[sizeof(text) - 1] = '\0'; - label->setText(text); -} - -void SfizzVstEditor::updateNumVoicesLabel(int numVoices) -{ - CTextLabel* label = _numVoicesLabel; - if (!label) - return; - - char text[64]; - sprintf(text, "%d", numVoices); - text[sizeof(text) - 1] = '\0'; - label->setText(text); -} - -void SfizzVstEditor::updateOversamplingLabel(int oversamplingLog2) -{ - CTextLabel* label = _oversamplingLabel; - if (!label) - return; - - char text[64]; - sprintf(text, "%dx", 1 << oversamplingLog2); - text[sizeof(text) - 1] = '\0'; - label->setText(text); -} - -void SfizzVstEditor::updatePreloadSizeLabel(int preloadSize) -{ - CTextLabel* label = _preloadSizeLabel; - if (!label) - return; - - char text[64]; - sprintf(text, "%.1f kB", preloadSize * (1.0 / 1024)); - text[sizeof(text) - 1] = '\0'; - label->setText(text); -} - -void SfizzVstEditor::updateScalaRootKeyLabel(int rootKey) -{ - CTextLabel* label = _scalaRootKeyLabel; - if (!label) - return; - - static const char *octNoteNames[12] = { - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", - }; - - auto noteName = [](int key) -> std::string - { - int octNum; - int octNoteNum; - if (key >= 0) { - octNum = key / 12 - 1; - octNoteNum = key % 12; - } - else { - octNum = -2 - (key + 1) / -12; - octNoteNum = (key % 12 + 12) % 12; - } - return std::string(octNoteNames[octNoteNum]) + std::to_string(octNum); - }; - - label->setText(noteName(rootKey)); -} - -void SfizzVstEditor::updateTuningFrequencyLabel(float tuningFrequency) -{ - CTextLabel* label = _tuningFrequencyLabel; - if (!label) - return; - - char text[64]; - sprintf(text, "%.1f", tuningFrequency); - text[sizeof(text) - 1] = '\0'; - label->setText(text); -} - -void SfizzVstEditor::updateStretchedTuningLabel(float stretchedTuning) -{ - CTextLabel* label = _stretchedTuningLabel; - if (!label) - return; - - char text[64]; - sprintf(text, "%.3f", stretchedTuning); - text[sizeof(text) - 1] = '\0'; - label->setText(text); -} - - -void SfizzVstEditor::setActivePanel(unsigned panelId) -{ - panelId = std::max(0, std::min(kNumPanels - 1, static_cast(panelId))); - - getController()->getSfizzUiState().activePanel = panelId; - - if (_activePanel != panelId) { - if (frame) - _subPanels[_activePanel]->setVisible(false); - - _activePanel = panelId; - - if (frame) - _subPanels[panelId]->setVisible(true); + switch (id) { + case EditId::Volume: return kPidVolume; + case EditId::Polyphony: return kPidNumVoices; + case EditId::Oversampling: return kPidOversampling; + case EditId::PreloadSize: return kPidPreloadSize; + case EditId::ScalaRootKey: return kPidScalaRootKey; + case EditId::TuningFrequency: return kPidTuningFrequency; + case EditId::StretchTuning: return kPidStretchedTuning; + default: return -1; } } diff --git a/vst/SfizzVstEditor.h b/vst/SfizzVstEditor.h index 21cf8a87..9c918765 100644 --- a/vst/SfizzVstEditor.h +++ b/vst/SfizzVstEditor.h @@ -6,7 +6,9 @@ #pragma once #include "SfizzVstController.h" +#include "editor/EditorController.h" #include "public.sdk/source/vst/vstguieditor.h" +class Editor; #if !defined(__APPLE__) && !defined(_WIN32) namespace VSTGUI { class RunLoop; } #endif @@ -14,7 +16,7 @@ namespace VSTGUI { class RunLoop; } using namespace Steinberg; using namespace VSTGUI; -class SfizzVstEditor : public Vst::VSTGUIEditor, public IControlListener, public SfizzVstController::StateListener { +class SfizzVstEditor : public Vst::VSTGUIEditor, public SfizzVstController::StateListener, public EditorController { public: explicit SfizzVstEditor(void *controller); ~SfizzVstEditor(); @@ -27,98 +29,28 @@ public: return static_cast(Vst::VSTGUIEditor::getController()); } - // IControlListener - void valueChanged(CControl* ctl) override; - void enterOrLeaveEdit(CControl* ctl, bool enter); - void controlBeginEdit(CControl* ctl) override; - void controlEndEdit(CControl* ctl) override; - // VSTGUIEditor CMessageResult notify(CBaseObject* sender, const char* message) override; // SfizzVstController::StateListener void onStateChanged() override; -private: - void chooseSfzFile(); - void loadSfzFile(const std::string& filePath); +protected: + // EditorController + void uiSendValue(EditId id, const EditValue& v) override; + void uiBeginSend(EditId id) override; + void uiEndSend(EditId id) override; + void uiSendMIDI(const uint8_t* msg, uint32_t len) override; - void chooseScalaFile(); +private: + void loadSfzFile(const std::string& filePath); void loadScalaFile(const std::string& filePath); - void createFrameContents(); void updateStateDisplay(); - void updateSfzFileLabel(const std::string& filePath); - void updateScalaFileLabel(const std::string& filePath); - static void updateLabelWithFileName(CTextLabel* label, const std::string& filePath); - void updateVolumeLabel(float volume); - void updateNumVoicesLabel(int numVoices); - void updateOversamplingLabel(int oversamplingLog2); - void updatePreloadSizeLabel(int preloadSize); - void updateScalaRootKeyLabel(int rootKey); - void updateTuningFrequencyLabel(float tuningFrequency); - void updateStretchedTuningLabel(float stretchedTuning); - void setActivePanel(unsigned panelId); - template - void adjustMinMaxToRangeParam(Control* c, Vst::ParamID id) - { - auto* p = static_cast(getController()->getParameterObject(id)); - c->setMin(p->getMin()); - c->setMax(p->getMax()); - c->setDefaultValue(p->toPlain(p->getInfo().defaultNormalizedValue)); - } + Vst::ParamID parameterOfEditId(EditId id); - enum { - kPanelGeneral, - // kPanelControls, - kPanelSettings, - kPanelTuning, - kPanelInfo, - kNumPanels, - }; - - unsigned _activePanel = 0; - CViewContainer* _subPanels[kNumPanels] = {}; - - enum { - kTagLoadSfzFile, - kTagSetVolume, - kTagSetNumVoices, - kTagSetOversampling, - kTagSetPreloadSize, - kTagLoadScalaFile, - kTagSetScalaRootKey, - kTagSetTuningFrequency, - kTagSetStretchedTuning, - kTagFirstChangePanel, - kTagLastChangePanel = kTagFirstChangePanel + kNumPanels - 1, - }; - - CBitmap _logo; - CTextLabel* _sfzFileLabel = nullptr; - CTextLabel* _scalaFileLabel = nullptr; - CSliderBase *_volumeSlider = nullptr; - CTextLabel* _volumeLabel = nullptr; - CSliderBase *_numVoicesSlider = nullptr; - CTextLabel* _numVoicesLabel = nullptr; - CSliderBase *_oversamplingSlider = nullptr; - CTextLabel* _oversamplingLabel = nullptr; - CSliderBase *_preloadSizeSlider = nullptr; - CTextLabel* _preloadSizeLabel = nullptr; - CSliderBase *_scalaRootKeySlider = nullptr; - CTextLabel* _scalaRootKeyLabel = nullptr; - CSliderBase *_tuningFrequencySlider = nullptr; - CTextLabel* _tuningFrequencyLabel = nullptr; - CSliderBase *_stretchedTuningSlider = nullptr; - CTextLabel* _stretchedTuningLabel = nullptr; - - CTextLabel* _infoCurvesLabel = nullptr; - CTextLabel* _infoMastersLabel = nullptr; - CTextLabel* _infoGroupsLabel = nullptr; - CTextLabel* _infoRegionsLabel = nullptr; - CTextLabel* _infoSamplesLabel = nullptr; - CTextLabel* _infoVoicesLabel = nullptr; + std::unique_ptr editor_; #if !defined(__APPLE__) && !defined(_WIN32) SharedPointer _runLoop; diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index bedde084..441fd9df 100644 --- a/vst/SfizzVstProcessor.cpp +++ b/vst/SfizzVstProcessor.cpp @@ -289,15 +289,15 @@ void SfizzVstProcessor::processParameterChanges(Vst::IParameterChanges& pc) break; case kPidScalaRootKey: if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) - _state.scalaRootKey = static_cast(kParamScalaRootKey.denormalize(value)); + _state.scalaRootKey = static_cast(kParamScalaRootKeyRange.denormalize(value)); break; case kPidTuningFrequency: if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) - _state.tuningFrequency = kParamTuningFrequency.denormalize(value); + _state.tuningFrequency = kParamTuningFrequencyRange.denormalize(value); break; case kPidStretchedTuning: if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) - _state.stretchedTuning = kParamStretchedTuning.denormalize(value); + _state.stretchedTuning = kParamStretchedTuningRange.denormalize(value); break; } } diff --git a/vst/SfizzVstState.h b/vst/SfizzVstState.h index f356d04e..531e2dac 100644 --- a/vst/SfizzVstState.h +++ b/vst/SfizzVstState.h @@ -99,6 +99,6 @@ static constexpr SfizzParameterRange kParamVolumeRange(0.0, -60.0, +6.0); static constexpr SfizzParameterRange kParamNumVoicesRange(64.0, 1.0, 256.0); static constexpr SfizzParameterRange kParamOversamplingRange(0.0, 0.0, 3.0); static constexpr SfizzParameterRange kParamPreloadSizeRange(8192.0, 1024.0, 65536.0); -static constexpr SfizzParameterRange kParamScalaRootKey(60.0, 0.0, 127.0); -static constexpr SfizzParameterRange kParamTuningFrequency(440.0, 300.0, 500.0); -static constexpr SfizzParameterRange kParamStretchedTuning(0.0, 0.0, 1.0); +static constexpr SfizzParameterRange kParamScalaRootKeyRange(60.0, 0.0, 127.0); +static constexpr SfizzParameterRange kParamTuningFrequencyRange(440.0, 300.0, 500.0); +static constexpr SfizzParameterRange kParamStretchedTuningRange(0.0, 0.0, 1.0); diff --git a/vst/cmake/Vst3.cmake b/vst/cmake/Vst3.cmake index 68118f73..259f62a2 100644 --- a/vst/cmake/Vst3.cmake +++ b/vst/cmake/Vst3.cmake @@ -63,222 +63,7 @@ endfunction() # --- VSTGUI --- function(plugin_add_vstgui NAME) - target_sources("${NAME}" PRIVATE - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/animation/animations.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/animation/animator.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/animation/timingfunctions.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cbitmap.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cbitmapfilter.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/ccolor.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cdatabrowser.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cdrawcontext.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cdrawmethods.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cdropsource.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cfileselector.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cfont.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cframe.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cgradientview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cgraphicspath.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/clayeredviewcontainer.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/clinestyle.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/coffscreencontext.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cautoanimation.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cbuttons.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/ccolorchooser.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/ccontrol.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cfontchooser.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cknob.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/clistcontrol.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cmoviebitmap.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cmoviebutton.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/coptionmenu.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cparamdisplay.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cscrollbar.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/csearchtextedit.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/csegmentbutton.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cslider.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cspecialdigit.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/csplashscreen.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cstringlist.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cswitch.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/ctextedit.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/ctextlabel.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cvumeter.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/controls/cxypad.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/copenglview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cpoint.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/crect.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/crowcolumnview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cscrollview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cshadowviewcontainer.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/csplitview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cstring.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/ctabview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/ctooltipsupport.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cviewcontainer.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/cvstguitimer.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/genericstringlistdatabrowsersource.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/common/genericoptionmenu.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/vstguidebug.cpp") - - if(WIN32) - target_sources("${NAME}" PRIVATE - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/common/fileresourceinputstream.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/direct2d/d2dbitmap.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/direct2d/d2ddrawcontext.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/direct2d/d2dfont.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/direct2d/d2dgraphicspath.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/win32datapackage.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/win32dragging.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/win32frame.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/win32openglview.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/win32optionmenu.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/win32support.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/win32textedit.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/winfileselector.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/winstring.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/win32/wintimer.cpp") - elseif(APPLE) - target_sources("${NAME}" PRIVATE - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/common/fileresourceinputstream.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/common/genericoptionmenu.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/common/generictextedit.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/carbon/hiviewframe.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/carbon/hiviewoptionmenu.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/carbon/hiviewtextedit.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/caviewlayer.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cfontmac.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cgbitmap.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cgdrawcontext.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cocoa/autoreleasepool.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cocoa/cocoahelpers.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cocoa/cocoaopenglview.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cocoa/cocoatextedit.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cocoa/nsviewdraggingsession.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cocoa/nsviewframe.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/cocoa/nsviewoptionmenu.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/macclipboard.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/macfileselector.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/macglobals.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/macstring.mm" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/mactimer.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/mac/quartzgraphicspath.cpp") - else() - target_sources("${NAME}" PRIVATE - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/common/fileresourceinputstream.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/common/generictextedit.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/cairobitmap.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/cairocontext.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/cairofont.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/cairogradient.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/cairopath.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/linuxstring.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/x11fileselector.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/x11frame.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/x11platform.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/x11timer.cpp" - "${VST3SDK_BASEDIR}/vstgui4/vstgui/lib/platform/linux/x11utils.cpp") - endif() - - target_include_directories("${NAME}" PRIVATE "${VST3SDK_BASEDIR}/vstgui4") - - if(WIN32) - target_compile_definitions("${NAME}" PRIVATE "NOMINMAX=1") - if (NOT MSVC) - # autolinked on MSVC with pragmas - find_library(OPENGL32_LIBRARY "opengl32") - find_library(D2D1_LIBRARY "d2d1") - find_library(DWRITE_LIBRARY "dwrite") - find_library(DWMAPI_LIBRARY "dwmapi") - find_library(WINDOWSCODECS_LIBRARY "windowscodecs") - find_library(SHLWAPI_LIBRARY "shlwapi") - target_link_libraries("${NAME}" PRIVATE - "${OPENGL32_LIBRARY}" - "${D2D1_LIBRARY}" - "${DWRITE_LIBRARY}" - "${DWMAPI_LIBRARY}" - "${WINDOWSCODECS_LIBRARY}" - "${SHLWAPI_LIBRARY}") - endif() - elseif(APPLE) - find_library(APPLE_COREFOUNDATION_LIBRARY "CoreFoundation") - find_library(APPLE_FOUNDATION_LIBRARY "Foundation") - find_library(APPLE_COCOA_LIBRARY "Cocoa") - find_library(APPLE_OPENGL_LIBRARY "OpenGL") - find_library(APPLE_ACCELERATE_LIBRARY "Accelerate") - find_library(APPLE_QUARTZCORE_LIBRARY "QuartzCore") - find_library(APPLE_CARBON_LIBRARY "Carbon") - find_library(APPLE_AUDIOTOOLBOX_LIBRARY "AudioToolbox") - find_library(APPLE_COREAUDIO_LIBRARY "CoreAudio") - find_library(APPLE_COREMIDI_LIBRARY "CoreMIDI") - target_link_libraries("${NAME}" PRIVATE - "${APPLE_COREFOUNDATION_LIBRARY}" - "${APPLE_FOUNDATION_LIBRARY}" - "${APPLE_COCOA_LIBRARY}" - "${APPLE_OPENGL_LIBRARY}" - "${APPLE_ACCELERATE_LIBRARY}" - "${APPLE_QUARTZCORE_LIBRARY}" - "${APPLE_CARBON_LIBRARY}" - "${APPLE_AUDIOTOOLBOX_LIBRARY}" - "${APPLE_COREAUDIO_LIBRARY}" - "${APPLE_COREMIDI_LIBRARY}") - else() - find_package(X11 REQUIRED) - find_package(Freetype REQUIRED) - find_package(PkgConfig REQUIRED) - pkg_check_modules(LIBXCB REQUIRED xcb) - pkg_check_modules(LIBXCB_UTIL REQUIRED xcb-util) - pkg_check_modules(LIBXCB_CURSOR REQUIRED xcb-cursor) - pkg_check_modules(LIBXCB_KEYSYMS REQUIRED xcb-keysyms) - pkg_check_modules(LIBXCB_XKB REQUIRED xcb-xkb) - pkg_check_modules(LIBXKB_COMMON REQUIRED xkbcommon) - pkg_check_modules(LIBXKB_COMMON_X11 REQUIRED xkbcommon-x11) - pkg_check_modules(CAIRO REQUIRED cairo) - pkg_check_modules(FONTCONFIG REQUIRED fontconfig) - target_include_directories("${NAME}" PRIVATE - ${X11_INCLUDE_DIRS} - ${FREETYPE_INCLUDE_DIRS} - ${LIBXCB_INCLUDE_DIRS} - ${LIBXCB_UTIL_INCLUDE_DIRS} - ${LIBXCB_CURSOR_INCLUDE_DIRS} - ${LIBXCB_KEYSYMS_INCLUDE_DIRS} - ${LIBXCB_XKB_INCLUDE_DIRS} - ${LIBXKB_COMMON_INCLUDE_DIRS} - ${LIBXKB_COMMON_X11_INCLUDE_DIRS} - ${CAIRO_INCLUDE_DIRS} - ${FONTCONFIG_INCLUDE_DIRS}) - target_link_libraries("${NAME}" PRIVATE - ${X11_LIBRARIES} - ${FREETYPE_LIBRARIES} - ${LIBXCB_LIBRARIES} - ${LIBXCB_UTIL_LIBRARIES} - ${LIBXCB_CURSOR_LIBRARIES} - ${LIBXCB_KEYSYMS_LIBRARIES} - ${LIBXCB_XKB_LIBRARIES} - ${LIBXKB_COMMON_LIBRARIES} - ${LIBXKB_COMMON_X11_LIBRARIES} - ${CAIRO_LIBRARIES} - ${FONTCONFIG_LIBRARIES}) - find_library(DL_LIBRARY "dl") - if(DL_LIBRARY) - target_link_libraries("${NAME}" PRIVATE "${DL_LIBRARY}") - endif() - endif() - - target_sources("${NAME}" PRIVATE - "${VST3SDK_BASEDIR}/public.sdk/source/vst/vstguieditor.cpp") - - target_include_directories("${NAME}" PRIVATE - external/steinberg/src) - + target_link_libraries("${NAME}" PRIVATE sfizz-vstgui) + target_sources("${NAME}" PRIVATE "${VST3SDK_BASEDIR}/public.sdk/source/vst/vstguieditor.cpp") target_compile_definitions("${NAME}" PRIVATE "SMTG_MODULE_IS_BUNDLE=1") - - if(${CMAKE_BUILD_TYPE} MATCHES "Debug") - target_compile_definitions("${NAME}" PRIVATE "DEVELOPMENT") - endif() - - if(${CMAKE_BUILD_TYPE} MATCHES "Release") - target_compile_definitions("${NAME}" PRIVATE "RELEASE") - endif() endfunction() From b92209de7468e5cddf70fe9b986087293f852170 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 05:47:37 +0200 Subject: [PATCH 156/445] Support UI in LV2 --- .travis/prepare_osx.sh | 3 +- editor/CMakeLists.txt | 7 +- editor/cmake/Vstgui.cmake | 11 + {vst => editor}/resources/logo.png | Bin {vst => editor}/resources/logo.svg | 0 editor/src/editor/Editor.cpp | 2 +- lv2/CMakeLists.txt | 50 ++- lv2/lv2ui.version | 4 + lv2/manifest.ttl.in | 8 +- lv2/sfizz.c | 61 ++-- lv2/sfizz.ttl.in | 69 ++++ lv2/sfizz_lv2.h | 43 +++ lv2/sfizz_ui.cpp | 492 +++++++++++++++++++++++++++++ lv2/sfizz_ui.ttl.in | 14 + lv2/vstgui_helpers.cpp | 170 ++++++++++ lv2/vstgui_helpers.h | 68 ++++ vst/CMakeLists.txt | 7 +- 17 files changed, 966 insertions(+), 43 deletions(-) rename {vst => editor}/resources/logo.png (100%) rename {vst => editor}/resources/logo.svg (100%) create mode 100644 lv2/lv2ui.version create mode 100644 lv2/sfizz_lv2.h create mode 100644 lv2/sfizz_ui.cpp create mode 100644 lv2/sfizz_ui.ttl.in create mode 100644 lv2/vstgui_helpers.cpp create mode 100644 lv2/vstgui_helpers.h diff --git a/.travis/prepare_osx.sh b/.travis/prepare_osx.sh index 8fc891c9..8de6d08e 100755 --- a/.travis/prepare_osx.sh +++ b/.travis/prepare_osx.sh @@ -15,7 +15,8 @@ buildenv make DESTDIR=${PWD}/${INSTALL_DIR} install # Bundle LV2 dependencies cd "${INSTALL_DIR}"/Library/Audio/Plug-Ins/LV2 -dylibbundler -od -b -x sfizz.lv2/sfizz.so -d sfizz.lv2/libs/ -p @loader_path/libs/ +dylibbundler -od -b -x sfizz.lv2/Contents/Binary/sfizz.so -d sfizz.lv2/Contents/libs/ -p @loader_path/../libs/ +dylibbundler -od -b -x sfizz.lv2/Contents/Binary/sfizz_ui.so -d sfizz.lv2/Contents/libs/ -p @loader_path/../libs/ cd "${TRAVIS_BUILD_DIR}/build" # Bundle VST3 dependencies diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 977f36a4..777939d4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1,6 +1,10 @@ set(VSTGUI_BASEDIR "${CMAKE_CURRENT_SOURCE_DIR}/external/vstgui4") include("cmake/Vstgui.cmake") +set(EDITOR_RESOURCES + logo.png + PARENT_SCOPE) + # editor add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/EditIds.h @@ -13,4 +17,5 @@ add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/utility/vstgui_after.h src/editor/utility/vstgui_before.h) target_include_directories(sfizz_editor PUBLIC "src") -target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui absl::strings) +target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui) +target_link_libraries(sfizz_editor PUBLIC absl::strings absl::variant) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 92f85f03..e853b091 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -207,3 +207,14 @@ endif() if(${CMAKE_BUILD_TYPE} MATCHES "Release") target_compile_definitions(sfizz-vstgui PRIVATE "RELEASE") endif() + +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(sfizz-vstgui PRIVATE + "-Wno-deprecated-copy" + "-Wno-ignored-qualifiers" + "-Wno-reorder" + "-Wno-sign-compare" + "-Wno-unused-function" + "-Wno-unused-parameter" + "-Wno-unused-variable") +endif() diff --git a/vst/resources/logo.png b/editor/resources/logo.png similarity index 100% rename from vst/resources/logo.png rename to editor/resources/logo.png diff --git a/vst/resources/logo.svg b/editor/resources/logo.svg similarity index 100% rename from vst/resources/logo.svg rename to editor/resources/logo.svg diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index c4d247c8..1024c27d 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -288,7 +288,7 @@ void Editor::Impl::createFrameContents() view->setBackgroundColor(CColor(0xff, 0xff, 0xff)); - SharedPointer logo = new CBitmap("logo.png"); + SharedPointer logo { new CBitmap("logo.png") }; CRect bottomRow = bounds; bottomRow.top = bottomRow.bottom - 30; diff --git a/lv2/CMakeLists.txt b/lv2/CMakeLists.txt index 4104eb25..c45f964f 100644 --- a/lv2/CMakeLists.txt +++ b/lv2/CMakeLists.txt @@ -11,6 +11,10 @@ set (LV2PLUGIN_TTL_SRC_FILES manifest.ttl.in ${PROJECT_NAME}.ttl.in ) +if (SFIZZ_LV2_UI) + list(APPEND LV2PLUGIN_TTL_SRC_FILES + ${PROJECT_NAME}_ui.ttl.in) +endif() source_group("Turtle Files" FILES ${LV2PLUGIN_TTL_SRC_FILES} ) @@ -19,31 +23,61 @@ add_library (${LV2PLUGIN_PRJ_NAME} MODULE atomic_compat.h ${LV2PLUGIN_TTL_SRC_FILES}) target_link_libraries (${LV2PLUGIN_PRJ_NAME} ${PROJECT_NAME}::${PROJECT_NAME}) + +if (SFIZZ_LV2_UI) + add_library (${LV2PLUGIN_PRJ_NAME}_ui MODULE + ${PROJECT_NAME}_ui.cpp + vstgui_helpers.h + vstgui_helpers.cpp) + target_link_libraries (${LV2PLUGIN_PRJ_NAME}_ui sfizz_editor sfizz-vstgui) +endif() + # Explicitely strip all symbols on Linux but lv2_descriptor() # MacOS linker does not support this apparently https://bugs.webkit.org/show_bug.cgi?id=144555 if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") file(COPY lv2.version DESTINATION ${CMAKE_BINARY_DIR}/lv2) target_link_libraries(${LV2PLUGIN_PRJ_NAME} "-Wl,--version-script=lv2.version") # target_link_libraries(${LV2PLUGIN_PRJ_NAME} "-Wl,-u,lv2_descriptor") + if (SFIZZ_LV2_UI) + file(COPY lv2ui.version DESTINATION ${CMAKE_BINARY_DIR}/lv2) + target_link_libraries(${LV2PLUGIN_PRJ_NAME}_ui "-Wl,--version-script=lv2ui.version") + # target_link_libraries(${LV2PLUGIN_PRJ_NAME}_ui "-Wl,-u,lv2ui_descriptor") + endif() endif() target_include_directories(${LV2PLUGIN_PRJ_NAME} PRIVATE . external/ardour) sfizz_enable_lto_if_needed (${LV2PLUGIN_PRJ_NAME}) if (MINGW) set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LINK_FLAGS "-static") endif() +if (SFIZZ_LV2_UI) + target_include_directories(${LV2PLUGIN_PRJ_NAME}_ui PRIVATE . external/ardour) + sfizz_enable_lto_if_needed (${LV2PLUGIN_PRJ_NAME}_ui) + if (MINGW) + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES LINK_FLAGS "-static") + endif() +endif() # Remove the "lib" prefix, rename the target name and build it in the .lv build dir # /lv2/_lv2. to # /lv2/.lv2/. set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES PREFIX "") set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") -set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}") +set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary") + +if (SFIZZ_LV2_UI) + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES PREFIX "") + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES OUTPUT_NAME "${PROJECT_NAME}_ui") + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary") +endif() # Generate *.ttl files from *.in sources, # create the destination directory if it doesn't exists and copy needed files file (MAKE_DIRECTORY ${PROJECT_BINARY_DIR}) configure_file (manifest.ttl.in ${PROJECT_BINARY_DIR}/manifest.ttl) configure_file (${PROJECT_NAME}.ttl.in ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.ttl) +if (SFIZZ_LV2_UI) + configure_file (${PROJECT_NAME}_ui.ttl.in ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_ui.ttl) +endif() configure_file (LICENSE.md.in ${PROJECT_BINARY_DIR}/LICENSE.md) if (SFIZZ_USE_VCPKG OR SFIZZ_STATIC_DEPENDENCIES OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") file(COPY "lgpl-3.0.txt" DESTINATION ${PROJECT_BINARY_DIR}) @@ -54,12 +88,22 @@ set(LV2_RESOURCES DefaultInstrument.sfz DefaultScale.scl) execute_process( - COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/Resources") + COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/Contents/Resources") foreach(res ${LV2_RESOURCES}) file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources/${res}" - DESTINATION "${PROJECT_BINARY_DIR}/Resources") + DESTINATION "${PROJECT_BINARY_DIR}/Contents/Resources") endforeach() +# Copy editor resources +if (SFIZZ_LV2_UI) + execute_process ( + COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/Contents/Resources") + foreach(res ${EDITOR_RESOURCES}) + file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources/${res}" + DESTINATION "${PROJECT_BINARY_DIR}/Contents/Resources") + endforeach() +endif() + # Installation if (NOT MSVC) install (DIRECTORY ${PROJECT_BINARY_DIR} DESTINATION ${LV2PLUGIN_INSTALL_DIR} diff --git a/lv2/lv2ui.version b/lv2/lv2ui.version new file mode 100644 index 00000000..95c55e1b --- /dev/null +++ b/lv2/lv2ui.version @@ -0,0 +1,4 @@ +LV2UIABI_1.0 { + global: *lv2ui_descriptor*; + local: *; +}; diff --git a/lv2/manifest.ttl.in b/lv2/manifest.ttl.in index 3ea4e883..ab407d33 100644 --- a/lv2/manifest.ttl.in +++ b/lv2/manifest.ttl.in @@ -1,7 +1,13 @@ @prefix lv2: . @prefix rdfs: . +@prefix ui: . <@LV2PLUGIN_URI@> a lv2:Plugin ; - lv2:binary <@PROJECT_NAME@@CMAKE_SHARED_MODULE_SUFFIX@> ; + lv2:binary ; rdfs:seeAlso <@PROJECT_NAME@.ttl> . + +@LV2PLUGIN_IF_ENABLE_UI@<@LV2PLUGIN_URI@#ui> +@LV2PLUGIN_IF_ENABLE_UI@ a ui:@LV2_UI_TYPE@ ; +@LV2PLUGIN_IF_ENABLE_UI@ ui:binary ; +@LV2PLUGIN_IF_ENABLE_UI@ rdfs:seeAlso <@PROJECT_NAME@_ui.ttl> . diff --git a/lv2/sfizz.c b/lv2/sfizz.c index ef07343e..b68c527f 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -32,6 +32,9 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include "sfizz_lv2.h" + +#include #include #include #include @@ -58,23 +61,11 @@ #include "atomic_compat.h" -#define SFIZZ_URI "http://sfztools.github.io/sfizz" -#define SFIZZ_PREFIX SFIZZ_URI "#" -#define SFIZZ__sfzFile SFIZZ_URI ":" "sfzfile" -#define SFIZZ__tuningfile SFIZZ_URI ":" "tuningfile" -#define SFIZZ__numVoices SFIZZ_URI ":" "numvoices" -#define SFIZZ__preloadSize SFIZZ_URI ":" "preload_size" -#define SFIZZ__oversampling SFIZZ_URI ":" "oversampling" -// These ones are just for the worker -#define SFIZZ__logStatus SFIZZ_URI ":" "log_status" -#define SFIZZ__checkModification SFIZZ_URI ":" "check_modification" - #define CHANNEL_MASK 0x0F #define MIDI_CHANNEL(byte) (byte & CHANNEL_MASK) #define MIDI_STATUS(byte) (byte & ~CHANNEL_MASK) #define PITCH_BUILD_AND_CENTER(first_byte, last_byte) (int)(((unsigned int)last_byte << 7) + (unsigned int)first_byte) - 8192 #define MAX_BLOCK_SIZE 8192 -#define MAX_PATH_SIZE 1024 #define MAX_VOICES 256 #define DEFAULT_VOICES 64 #define DEFAULT_OVERSAMPLING SFIZZ_OVERSAMPLING_X1 @@ -82,8 +73,8 @@ #define LOG_SAMPLE_COUNT 48000 #define UNUSED(x) (void)(x) -#define DEFAULT_SCALA_FILE "Resources/DefaultScale.scl" -#define DEFAULT_SFZ_FILE "Resources/DefaultInstrument.sfz" +#define DEFAULT_SCALA_FILE "Contents/Resources/DefaultScale.scl" +#define DEFAULT_SFZ_FILE "Contents/Resources/DefaultInstrument.sfz" // This assumes that the longest path is the default sfz file; if not, change it #define MAX_BUNDLE_PATH_SIZE (MAX_PATH_SIZE - sizeof(DEFAULT_SFZ_FILE)) @@ -115,6 +106,11 @@ typedef struct const float *tuning_frequency_port; const float *stretch_tuning_port; float *active_voices_port; + float *num_curves_port; + float *num_masters_port; + float *num_groups_port; + float *num_regions_port; + float *num_samples_port; // Atom forge LV2_Atom_Forge forge; ///< Forge for writing atoms in run thread @@ -187,23 +183,6 @@ typedef struct char bundle_path[MAX_BUNDLE_PATH_SIZE]; } sfizz_plugin_t; -enum -{ - SFIZZ_CONTROL = 0, - SFIZZ_NOTIFY = 1, - SFIZZ_LEFT = 2, - SFIZZ_RIGHT = 3, - SFIZZ_VOLUME = 4, - SFIZZ_POLYPHONY = 5, - SFIZZ_OVERSAMPLING = 6, - SFIZZ_PRELOAD = 7, - SFIZZ_FREEWHEELING = 8, - SFIZZ_SCALA_ROOT_KEY = 9, - SFIZZ_TUNING_FREQUENCY = 10, - SFIZZ_STRETCH_TUNING = 11, - SFIZZ_ACTIVE_VOICES = 12, -}; - enum { SFIZZ_TIMEINFO_POSITION = 1 << 0, @@ -369,6 +348,21 @@ connect_port(LV2_Handle instance, case SFIZZ_ACTIVE_VOICES: self->active_voices_port = (float *)data; break; + case SFIZZ_NUM_CURVES: + self->num_curves_port = (float *)data; + break; + case SFIZZ_NUM_MASTERS: + self->num_masters_port = (float *)data; + break; + case SFIZZ_NUM_GROUPS: + self->num_groups_port = (float *)data; + break; + case SFIZZ_NUM_REGIONS: + self->num_regions_port = (float *)data; + break; + case SFIZZ_NUM_SAMPLES: + self->num_samples_port = (float *)data; + break; default: break; } @@ -977,6 +971,11 @@ run(LV2_Handle instance, uint32_t sample_count) sfizz_lv2_check_oversampling(self); sfizz_lv2_check_num_voices(self); *(self->active_voices_port) = sfizz_get_num_active_voices(self->synth); + *(self->num_curves_port) = sfizz_get_num_curves(self->synth); + *(self->num_masters_port) = sfizz_get_num_masters(self->synth); + *(self->num_groups_port) = sfizz_get_num_groups(self->synth); + *(self->num_regions_port) = sfizz_get_num_regions(self->synth); + *(self->num_samples_port) = sfizz_get_num_preloaded_samples(self->synth); // Log the buffer usage self->sample_counter += (int)sample_count; diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index b0934ca6..f4090158 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -13,6 +13,7 @@ @prefix rdfs: . @prefix state: . @prefix time: . +@prefix ui: . @prefix units: . @prefix urid: . @prefix work: . @@ -83,6 +84,8 @@ midnam:update a lv2:Feature . opts:supportedOption param:sampleRate ; opts:supportedOption bufsize:maxBlockLength, bufsize:nominalBlockLength ; + @LV2PLUGIN_IF_ENABLE_UI@ui:ui <@LV2PLUGIN_URI@#ui> ; + patch:writable <@LV2PLUGIN_URI@:sfzfile> , <@LV2PLUGIN_URI@:tuningfile> ; @@ -310,4 +313,70 @@ midnam:update a lv2:Feature . lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 256 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 12 ; + lv2:symbol "active_voices" ; + lv2:name "Active voices", + "Voix utilisées"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 256 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 13 ; + lv2:symbol "num_curves" ; + lv2:name "Number of curves", + "Nombre de courbes"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 14 ; + lv2:symbol "num_masters" ; + lv2:name "Number of masters", + "Nombre de maîtres"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 15 ; + lv2:symbol "num_groups" ; + lv2:name "Number of groups", + "Nombre de groupes"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 16 ; + lv2:symbol "num_regions" ; + lv2:name "Number of regions", + "Nombre de regions"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 17 ; + lv2:symbol "num_samples" ; + lv2:name "Number of samples", + "Nombre d'échantillons"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; ] . diff --git a/lv2/sfizz_lv2.h b/lv2/sfizz_lv2.h new file mode 100644 index 00000000..b0eafe00 --- /dev/null +++ b/lv2/sfizz_lv2.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once + +#define MAX_PATH_SIZE 1024 + +#define SFIZZ_URI "http://sfztools.github.io/sfizz" +#define SFIZZ_UI_URI "http://sfztools.github.io/sfizz#ui" +#define SFIZZ_PREFIX SFIZZ_URI "#" +#define SFIZZ__sfzFile SFIZZ_URI ":" "sfzfile" +#define SFIZZ__tuningfile SFIZZ_URI ":" "tuningfile" +#define SFIZZ__numVoices SFIZZ_URI ":" "numvoices" +#define SFIZZ__preloadSize SFIZZ_URI ":" "preload_size" +#define SFIZZ__oversampling SFIZZ_URI ":" "oversampling" +// These ones are just for the worker +#define SFIZZ__logStatus SFIZZ_URI ":" "log_status" +#define SFIZZ__checkModification SFIZZ_URI ":" "check_modification" + +enum +{ + SFIZZ_CONTROL = 0, + SFIZZ_NOTIFY = 1, + SFIZZ_LEFT = 2, + SFIZZ_RIGHT = 3, + SFIZZ_VOLUME = 4, + SFIZZ_POLYPHONY = 5, + SFIZZ_OVERSAMPLING = 6, + SFIZZ_PRELOAD = 7, + SFIZZ_FREEWHEELING = 8, + SFIZZ_SCALA_ROOT_KEY = 9, + SFIZZ_TUNING_FREQUENCY = 10, + SFIZZ_STRETCH_TUNING = 11, + SFIZZ_ACTIVE_VOICES = 12, + SFIZZ_NUM_CURVES = 13, + SFIZZ_NUM_MASTERS = 14, + SFIZZ_NUM_GROUPS = 15, + SFIZZ_NUM_REGIONS = 16, + SFIZZ_NUM_SAMPLES = 17, +}; diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp new file mode 100644 index 00000000..1cbb49a2 --- /dev/null +++ b/lv2/sfizz_ui.cpp @@ -0,0 +1,492 @@ +/* + SPDX-License-Identifier: ISC + + Sfizz LV2 plugin + + Copyright 2019-2020, Paul Ferrand + + This file was based on skeleton and example code from the LV2 plugin + distribution available at http://lv2plug.in/ + + The LV2 sample plugins have the following copyright and notice, which are + extended to the current work: + Copyright 2011-2016 David Robillard + Copyright 2011 Gabriel M. Beddingfield + Copyright 2011 James Morris + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + Compiling this plugin statically against libsndfile implies distributing it + under the terms of the LGPL v3 license. See the LICENSE.md file for more + information. If you did not receive a LICENSE.md file, inform the current + maintainer. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include "sfizz_lv2.h" + +#include "editor/Editor.h" +#include "editor/EditorController.h" +#include "editor/EditIds.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vstgui_helpers.h" + +#include "editor/utility/vstgui_before.h" +#include "vstgui/lib/cframe.h" +#include "vstgui/lib/platform/iplatformframe.h" +#include "editor/utility/vstgui_after.h" +using namespace VSTGUI; + +/// +struct FrameHolderDeleter { + void operator()(CFrame* frame) const + { + if (frame->getNbReference() != 1) + frame->forget(); + else + frame->close(); + } +}; +typedef std::unique_ptr FrameHolder; + +/// +struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { + LV2UI_Write_Function write = nullptr; + LV2UI_Controller con = nullptr; + LV2_URID_Map *map = nullptr; + LV2_URID_Unmap *unmap = nullptr; + LV2UI_Resize *resize = nullptr; + LV2UI_Touch *touch = nullptr; + std::unique_ptr editor; + FrameHolder uiFrame; +#if LINUX + SharedPointer runLoop; +#endif + + /// VSTGUIEditorInterface + CFrame* getFrame() const override { return uiFrame.get(); } + + LV2_Atom_Forge atom_forge; + LV2_URID atom_event_transfer_uri; + LV2_URID atom_object_uri; + LV2_URID atom_path_uri; + LV2_URID atom_urid_uri; + LV2_URID midi_event_uri; + LV2_URID patch_get_uri; + LV2_URID patch_set_uri; + LV2_URID patch_property_uri; + LV2_URID patch_value_uri; + LV2_URID sfizz_sfz_file_uri; + LV2_URID sfizz_scala_file_uri; + +protected: + void uiSendValue(EditId id, const EditValue& v) override; + void uiBeginSend(EditId id) override; + void uiEndSend(EditId id) override; + void uiSendMIDI(const uint8_t* msg, uint32_t len) override; + +private: + void uiTouch(EditId id, bool t); +}; + +static LV2UI_Handle +instantiate(const LV2UI_Descriptor *descriptor, + const char *plugin_uri, + const char *bundle_path, + LV2UI_Write_Function write_function, + LV2UI_Controller controller, + LV2UI_Widget *widget, + const LV2_Feature * const *features) +{ + std::unique_ptr self { new sfizz_ui_t }; + + (void)descriptor; + (void)plugin_uri; + (void)bundle_path; + + self->write = write_function; + self->con = controller; + + void *parentWindowId = nullptr; + + LV2_URID_Map *map = nullptr; + LV2_URID_Unmap *unmap = nullptr; + + for (const LV2_Feature *const *f = features; *f; f++) + { + if (!strcmp((**f).URI, LV2_URID__map)) + self->map = map = (LV2_URID_Map *)(**f).data; + else if (!strcmp((**f).URI, LV2_URID__unmap)) + self->unmap = unmap = (LV2_URID_Unmap *)(**f).data; + else if (!strcmp((**f).URI, LV2_UI__resize)) + self->resize = (LV2UI_Resize *)(**f).data; + else if (!strcmp((**f).URI, LV2_UI__touch)) + self->touch = (LV2UI_Touch*)(**f).data; + else if (!strcmp((**f).URI, LV2_UI__parent)) + parentWindowId = (**f).data; + } + + // The map feature is required + if (!map || !unmap) + return nullptr; + + LV2_Atom_Forge *forge = &self->atom_forge; + lv2_atom_forge_init(forge, map); + self->atom_event_transfer_uri = map->map(map->handle, LV2_ATOM__eventTransfer); + self->atom_object_uri = map->map(map->handle, LV2_ATOM__Object); + self->atom_path_uri = map->map(map->handle, LV2_ATOM__Path); + self->atom_urid_uri = map->map(map->handle, LV2_ATOM__URID); + self->midi_event_uri = map->map(map->handle, LV2_MIDI__MidiEvent); + self->patch_get_uri = map->map(map->handle, LV2_PATCH__Get); + self->patch_set_uri = map->map(map->handle, LV2_PATCH__Set); + self->patch_property_uri = map->map(map->handle, LV2_PATCH__property); + self->patch_value_uri = map->map(map->handle, LV2_PATCH__value); + self->sfizz_sfz_file_uri = map->map(map->handle, SFIZZ__sfzFile); + self->sfizz_scala_file_uri = map->map(map->handle, SFIZZ__tuningfile); + + // set up the resource path + // * on Linux, this is determined by going 2 folders back from the SO path + // name, and appending "Contents/Resources" (not overridable) + // * on Windows, the folder is set programmatically + // * on macOS, resource files are looked up using CFBundle APIs +#if _WIN32 + IWin32PlatformFrame::setResourceBasePath((std::string(bundle_path) + "\\Contents\\Resources\\").c_str()); +#elif __APPLE__ + #pragma message("TODO: make resources work on macOS using bundles") +#endif + + // makes labels refresh correctly + CView::kDirtyCallAlwaysOnMainThread = true; + + const CRect uiBounds(0, 0, Editor::viewWidth, Editor::viewHeight); + CFrame* uiFrame = new CFrame(uiBounds, self.get()); + self->uiFrame.reset(uiFrame); + + IPlatformFrameConfig* config = nullptr; +#if LINUX + SharedPointer runLoop = new Lv2IdleRunLoop; + self->runLoop = runLoop; + VSTGUI::X11::FrameConfig x11Config; + x11Config.runLoop = runLoop; + config = &x11Config; +#endif + + if (!uiFrame->open(parentWindowId, kDefaultNative, config)) + return nullptr; + + Editor *editor = new Editor(*self); + self->editor.reset(editor); + editor->open(*uiFrame); + + *widget = reinterpret_cast(uiFrame->getPlatformFrame()->getPlatformRepresentation()); + + if (self->resize) + self->resize->ui_resize(self->resize->handle, Editor::viewWidth, Editor::viewHeight); + + // send a request to receive all parameters + uint8_t buffer[256]; + lv2_atom_forge_set_buffer(forge, buffer, sizeof(buffer)); + LV2_Atom_Forge_Frame frame; + LV2_Atom *msg = (LV2_Atom *)lv2_atom_forge_object(forge, &frame, 0, self->patch_get_uri); + lv2_atom_forge_pop(forge, &frame); + write_function(controller, 0, lv2_atom_total_size(msg), self->atom_event_transfer_uri, msg); + + return self.release(); +} + +static void +cleanup(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + delete self; +} + +static void +port_event(LV2UI_Handle ui, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void *buffer) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + + if (format == 0) { + const float v = *reinterpret_cast(buffer); + + switch (port_index) { + case SFIZZ_VOLUME: + self->uiReceiveValue(EditId::Volume, v); + break; + case SFIZZ_POLYPHONY: + self->uiReceiveValue(EditId::Polyphony, v); + break; + case SFIZZ_OVERSAMPLING: + self->uiReceiveValue(EditId::Oversampling, v); + break; + case SFIZZ_PRELOAD: + self->uiReceiveValue(EditId::PreloadSize, v); + break; + case SFIZZ_SCALA_ROOT_KEY: + self->uiReceiveValue(EditId::ScalaRootKey, v); + break; + case SFIZZ_TUNING_FREQUENCY: + self->uiReceiveValue(EditId::TuningFrequency, v); + break; + case SFIZZ_STRETCH_TUNING: + self->uiReceiveValue(EditId::StretchTuning, v); + break; + case SFIZZ_ACTIVE_VOICES: + self->uiReceiveValue(EditId::UINumActiveVoices, v); + break; + case SFIZZ_NUM_CURVES: + self->uiReceiveValue(EditId::UINumCurves, v); + break; + case SFIZZ_NUM_MASTERS: + self->uiReceiveValue(EditId::UINumMasters, v); + break; + case SFIZZ_NUM_GROUPS: + self->uiReceiveValue(EditId::UINumGroups, v); + break; + case SFIZZ_NUM_REGIONS: + self->uiReceiveValue(EditId::UINumRegions, v); + break; + case SFIZZ_NUM_SAMPLES: + self->uiReceiveValue(EditId::UINumPreloadedSamples, v); + break; + } + } + else if (format == self->atom_event_transfer_uri) { + auto *atom = reinterpret_cast(buffer); + + if (atom->type == self->atom_object_uri) { + const LV2_Atom *prop = nullptr; + const LV2_Atom *value = nullptr; + + lv2_atom_object_get( + reinterpret_cast(atom), + self->patch_property_uri, &prop, self->patch_value_uri, &value, 0); + + if (prop && value && prop->type == self->atom_urid_uri) { + const LV2_URID prop_uri = reinterpret_cast(prop)->body; + auto *value_body = reinterpret_cast(LV2_ATOM_BODY_CONST(value)); + + if (prop_uri == self->sfizz_sfz_file_uri && value->type == self->atom_path_uri) { + std::string path(value_body, strnlen(value_body, value->size)); + self->uiReceiveValue(EditId::SfzFile, path); + } + else if (prop_uri == self->sfizz_scala_file_uri && value->type == self->atom_path_uri) { + std::string path(value_body, strnlen(value_body, value->size)); + self->uiReceiveValue(EditId::ScalaFile, path); + } + } + } + } + + (void)buffer_size; +} + +static int +idle(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + +#if LINUX + self->runLoop->execIdle(); +#endif + + return 0; +} + +static const LV2UI_Idle_Interface idle_interface = { + &idle, +}; + +static int +show(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + self->uiFrame->setVisible(true); + return 0; +} + +static int +hide(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + self->uiFrame->setVisible(false); + return 0; +} + +static const LV2UI_Show_Interface show_interface = { + &show, + &hide, +}; + +const void * +extension_data(const char *uri) +{ + if (!strcmp(uri, LV2_UI__idleInterface)) + return &idle_interface; + + if (!strcmp(uri, LV2_UI__showInterface)) + return &show_interface; + + return nullptr; +} + +static const LV2UI_Descriptor descriptor = { + SFIZZ_UI_URI, + instantiate, + cleanup, + port_event, + extension_data, +}; + +LV2_SYMBOL_EXPORT +const LV2UI_Descriptor * +lv2ui_descriptor(uint32_t index) +{ +#if LINUX + VSTGUI::initializeSoHandle(); +#endif + + switch (index) + { + case 0: + return &descriptor; + default: + return nullptr; + } +} + +/// +void sfizz_ui_t::uiSendValue(EditId id, const EditValue& v) +{ + auto sendFloat = [this](int port, float value) { + write(con, port, sizeof(float), 0, &value); + }; + + auto sendPath = [this](LV2_URID property, const std::string& value) { + LV2_Atom_Forge *forge = &atom_forge; + LV2_Atom_Forge_Frame frame; + alignas(LV2_Atom) uint8_t buffer[MAX_PATH_SIZE + 512]; + auto *atom = reinterpret_cast(buffer); + lv2_atom_forge_set_buffer(forge, (uint8_t *)&buffer, sizeof(buffer)); + if (lv2_atom_forge_object(forge, &frame, 0, patch_set_uri) && + lv2_atom_forge_key(forge, patch_property_uri) && + lv2_atom_forge_urid(forge, property) && + lv2_atom_forge_key(forge, patch_value_uri) && + lv2_atom_forge_path(forge, value.data(), value.size())) + { + lv2_atom_forge_pop(forge, &frame); + write(con, SFIZZ_CONTROL, lv2_atom_total_size(atom), atom_event_transfer_uri, atom); + } + }; + + switch (id) { + case EditId::Volume: + sendFloat(SFIZZ_VOLUME, absl::get(v)); + break; + case EditId::Polyphony: + sendFloat(SFIZZ_POLYPHONY, absl::get(v)); + break; + case EditId::Oversampling: + sendFloat(SFIZZ_OVERSAMPLING, absl::get(v)); + break; + case EditId::PreloadSize: + sendFloat(SFIZZ_PRELOAD, absl::get(v)); + break; + case EditId::ScalaRootKey: + sendFloat(SFIZZ_SCALA_ROOT_KEY, absl::get(v)); + break; + case EditId::TuningFrequency: + sendFloat(SFIZZ_TUNING_FREQUENCY, absl::get(v)); + break; + case EditId::StretchTuning: + sendFloat(SFIZZ_STRETCH_TUNING, absl::get(v)); + break; + case EditId::SfzFile: + sendPath(sfizz_sfz_file_uri, absl::get(v)); + break; + case EditId::ScalaFile: + sendPath(sfizz_scala_file_uri, absl::get(v)); + break; + default: + break; + } +} + +void sfizz_ui_t::uiBeginSend(EditId id) +{ + uiTouch(id, true); +} + +void sfizz_ui_t::uiEndSend(EditId id) +{ + uiTouch(id, false); +} + +void sfizz_ui_t::uiTouch(EditId id, bool t) +{ + if (!touch) + return; + + switch (id) { + case EditId::Volume: + touch->touch(touch->handle, SFIZZ_VOLUME, t); + break; + case EditId::Polyphony: + touch->touch(touch->handle, SFIZZ_POLYPHONY, t); + break; + case EditId::Oversampling: + touch->touch(touch->handle, SFIZZ_OVERSAMPLING, t); + break; + case EditId::PreloadSize: + touch->touch(touch->handle, SFIZZ_PRELOAD, t); + break; + case EditId::ScalaRootKey: + touch->touch(touch->handle, SFIZZ_SCALA_ROOT_KEY, t); + break; + case EditId::TuningFrequency: + touch->touch(touch->handle, SFIZZ_TUNING_FREQUENCY, t); + break; + case EditId::StretchTuning: + touch->touch(touch->handle, SFIZZ_STRETCH_TUNING, t); + break; + default: + break; + } +} + +void sfizz_ui_t::uiSendMIDI(const uint8_t* msg, uint32_t len) +{ + LV2_Atom_Forge *forge = &atom_forge; + alignas(LV2_Atom) uint8_t buffer[512]; + auto *atom = reinterpret_cast(buffer); + lv2_atom_forge_set_buffer(forge, (uint8_t *)&buffer, sizeof(buffer)); + if (lv2_atom_forge_atom(forge, len, midi_event_uri) && + lv2_atom_forge_write(forge, msg, len)) + { + write(con, SFIZZ_CONTROL, lv2_atom_total_size(atom), atom_event_transfer_uri, atom); + } +} diff --git a/lv2/sfizz_ui.ttl.in b/lv2/sfizz_ui.ttl.in new file mode 100644 index 00000000..b5fb3470 --- /dev/null +++ b/lv2/sfizz_ui.ttl.in @@ -0,0 +1,14 @@ +@prefix lv2: . +@prefix ui: . +@prefix urid: . + +<@LV2PLUGIN_URI@#ui> + lv2:extensionData ui:idleInterface ; + lv2:extensionData ui:showInterface ; + lv2:requiredFeature ui:idleInterface ; + lv2:optionalFeature ui:noUserResize ; + lv2:optionalFeature ui:resize ; + lv2:optionalFeature ui:parent ; + lv2:optionalFeature ui:touch ; + lv2:requiredFeature urid:map ; + lv2:requiredFeature urid:unmap . diff --git a/lv2/vstgui_helpers.cpp b/lv2/vstgui_helpers.cpp new file mode 100644 index 00000000..4e8969f6 --- /dev/null +++ b/lv2/vstgui_helpers.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +// Note(jpc) same code as used in Surge LV2, I am the original author + +#include "vstgui_helpers.h" +#include +#include +#include +#include +#if LINUX +#include +#include +#endif + +#if LINUX +void Lv2IdleRunLoop::execIdle() +{ + std::chrono::steady_clock::time_point tick = std::chrono::steady_clock::now(); + + for (Event& ev : _events) + { + if (!ev.alive) + continue; + +// TODO LV2: fix me, XCB descriptor polling not working at this point +#if 0 + pollfd pfd = {}; + pfd.fd = ev.fd; + pfd.events = POLLIN|POLLERR|POLLHUP; + if (poll(&pfd, 1, 0) > 0) +#endif + { + ev.handler->onEvent(); + } + } + + for (Timer& tm : _timers) + { + if (!tm.alive) + continue; + + if (tm.lastTickValid) + { + std::chrono::steady_clock::duration duration = tick - tm.lastTick; + tm.counter += std::chrono::duration_cast(duration); + if (tm.counter >= tm.interval) + { + tm.handler->onTimer(); + tm.counter = std::min(tm.counter - tm.interval, tm.interval); + } + } + tm.lastTick = tick; + tm.lastTickValid = true; + } + + garbageCollectDeadHandlers(_events); + garbageCollectDeadHandlers(_timers); +} + +bool Lv2IdleRunLoop::registerEventHandler(int fd, VSTGUI::X11::IEventHandler* handler) +{ + // fprintf(stderr, "registerEventHandler %d %p\n", fd, handler); + + Event ev; + ev.fd = fd; + ev.handler = handler; + ev.alive = true; + _events.push_back(ev); + + return true; +} + +bool Lv2IdleRunLoop::unregisterEventHandler(VSTGUI::X11::IEventHandler* handler) +{ + // fprintf(stderr, "unregisterEventHandler %p\n", handler); + + auto it = std::find_if(_events.begin(), _events.end(), [handler](const Event& ev) -> bool { + return ev.handler == handler && ev.alive; + }); + + if (it != _events.end()) + it->alive = false; + + return true; +} + +bool Lv2IdleRunLoop::registerTimer(uint64_t interval, VSTGUI::X11::ITimerHandler* handler) +{ + // fprintf(stderr, "registerTimer %lu %p\n", interval, handler); + + Timer tm; + tm.interval = std::chrono::milliseconds(interval); + tm.counter = std::chrono::microseconds(0); + tm.lastTickValid = false; + tm.handler = handler; + tm.alive = true; + _timers.push_back(tm); + + return true; +} + +bool Lv2IdleRunLoop::unregisterTimer(VSTGUI::X11::ITimerHandler* handler) +{ + // fprintf(stderr, "unregisterTimer %p\n", handler); + + auto it = std::find_if(_timers.begin(), _timers.end(), [handler](const Timer& tm) -> bool { + return tm.handler == handler && tm.alive; + }); + + if (it != _timers.end()) + it->alive = false; + + return true; +} + +template void Lv2IdleRunLoop::garbageCollectDeadHandlers(std::list& handlers) +{ + auto pos = handlers.begin(); + auto end = handlers.end(); + + while (pos != end) + { + auto curPos = pos++; + if (!curPos->alive) + handlers.erase(curPos); + } +} +#endif + +/// +#if LINUX +namespace VSTGUI +{ +void* soHandle = nullptr; + +static volatile bool soHandleInitialized = false; +static std::mutex soHandleMutex; + +struct Dl_handle_deleter +{ + void operator()(void* x) const noexcept + { + dlclose(x); + } +}; +static std::unique_ptr soHandlePointer; +} // namespace VSTGUI + +void VSTGUI::initializeSoHandle() +{ + if (VSTGUI::soHandleInitialized) + return; + + std::lock_guard lock(VSTGUI::soHandleMutex); + if (VSTGUI::soHandleInitialized) + return; + + Dl_info info; + if (dladdr((void*)&lv2ui_descriptor, &info)) + { + VSTGUI::soHandle = dlopen(info.dli_fname, RTLD_LAZY); + VSTGUI::soHandlePointer.reset(VSTGUI::soHandle); + } + VSTGUI::soHandleInitialized = true; +} +#endif diff --git a/lv2/vstgui_helpers.h b/lv2/vstgui_helpers.h new file mode 100644 index 00000000..bb48936e --- /dev/null +++ b/lv2/vstgui_helpers.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +// Note(jpc) same code as used in Surge LV2, I am the original author + +#pragma once +#include +#include + +#include "editor/utility/vstgui_before.h" +#include "vstgui/lib/vstguibase.h" +#if LINUX +#include "vstgui/lib/platform/platform_x11.h" +#include "vstgui/lib/platform/linux/x11platform.h" +#endif +#include "editor/utility/vstgui_after.h" + +#if LINUX +class Lv2IdleRunLoop : public VSTGUI::X11::IRunLoop +{ +public: + void execIdle(); + + bool registerEventHandler(int fd, VSTGUI::X11::IEventHandler* handler) override; + bool unregisterEventHandler(VSTGUI::X11::IEventHandler* handler) override; + bool registerTimer(uint64_t interval, VSTGUI::X11::ITimerHandler* handler) override; + bool unregisterTimer(VSTGUI::X11::ITimerHandler* handler) override; + + void forget() override + {} + void remember() override + {} + +private: + struct Event + { + int fd; + VSTGUI::X11::IEventHandler* handler; + bool alive; + }; + struct Timer + { + std::chrono::microseconds interval; + std::chrono::microseconds counter; + bool lastTickValid; + std::chrono::steady_clock::time_point lastTick; + VSTGUI::X11::ITimerHandler* handler; + bool alive; + }; + +private: + template static void garbageCollectDeadHandlers(std::list& handlers); + +private: + std::list _events; + std::list _timers; +}; +#endif + +#if LINUX +namespace VSTGUI +{ +void initializeSoHandle(); +} +#endif diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index 5d0301e6..f96abd80 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -27,9 +27,6 @@ set(VSTPLUGIN_HEADERS SfizzVstState.h X11RunLoop.h) -set(VSTPLUGIN_RESOURCES - logo.png) - add_library(${VSTPLUGIN_PRJ_NAME} MODULE ${VSTPLUGIN_HEADERS} ${VSTPLUGIN_SOURCES}) @@ -71,8 +68,8 @@ endif() # Create the bundle (see "VST 3 Locations / Format") execute_process ( COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") -foreach(res ${VSTPLUGIN_RESOURCES}) - file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources/${res}" +foreach(res ${EDITOR_RESOURCES}) + file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources/${res}" DESTINATION "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") endforeach() if(WIN32) From ecde6a4baf7d03cb53c4fae1db1e88ee366c8ffe Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 09:37:39 +0200 Subject: [PATCH 157/445] Allow clang-tidy to find new headers --- scripts/run_clang_tidy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/run_clang_tidy.sh b/scripts/run_clang_tidy.sh index a25f69f9..0f3ff24b 100755 --- a/scripts/run_clang_tidy.sh +++ b/scripts/run_clang_tidy.sh @@ -33,4 +33,5 @@ clang-tidy \ -- -Iexternal/abseil-cpp -Isrc/external -Isrc/external/pugixml/src \ -Isrc/sfizz -Isrc -Isrc/external/spline -Isrc/external/cpuid/src \ -Ivst -Ivst/external/VST_SDK/VST3_SDK -Ivst/external/VST_SDK/VST3_SDK/vstgui4 -Ivst/external/ring_buffer \ + -Ieditor/src \ -DNDEBUG -std=c++17 From 4c9249f713dceea38458a68e4e6cac29cc47d2cd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 09:42:04 +0200 Subject: [PATCH 158/445] Fix a narrowing conversion failing in MSVC --- editor/src/editor/Editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 1024c27d..ea2c8e57 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -282,7 +282,7 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) void Editor::Impl::createFrameContents() { - const CRect bounds { 0.0, 0.0, viewWidth, viewHeight }; + const CRect bounds { 0.0, 0.0, static_cast(viewWidth), static_cast(viewHeight) }; CViewContainer* view = new CViewContainer(bounds); view_ = view; From f5c735f0f1757edaf131ec75f00b5c42f46078c2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 09:44:43 +0200 Subject: [PATCH 159/445] appveyor: also copy lv2_ui --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index f1110ee1..d5e329a7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,6 +29,7 @@ build_script: after_build: - cmd: cp sfizz.lv2/Release/sfizz.dll sfizz.lv2/ +- cmd: cp sfizz.lv2/Release/sfizz_ui.dll sfizz.lv2/ - cmd: rm -rf sfizz.lv2/Release - cmd: if %platform%==Win32 set RELEASE_ARCH=x86 - cmd: if %platform%==x64 set RELEASE_ARCH=x64 From 1d58dafa2cd7cd35b98bef520b544289020b8852 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 10:02:41 +0200 Subject: [PATCH 160/445] Fix a problem with variant under minGW --- editor/src/editor/Editor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index ea2c8e57..edad917d 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -909,7 +909,7 @@ void Editor::Impl::valueChanged(CControl* ctl) break; case kTagSetOversampling: - ctrl.uiSendValue(EditId::Oversampling, 1 << static_cast(value)); + ctrl.uiSendValue(EditId::Oversampling, static_cast(1 << static_cast(value))); updateOversamplingLabel(static_cast(value)); break; @@ -936,7 +936,7 @@ void Editor::Impl::valueChanged(CControl* ctl) default: if (tag >= kTagFirstChangePanel && tag <= kTagLastChangePanel) { int panelId = tag - kTagFirstChangePanel; - ctrl.uiSendValue(EditId::UIActivePanel, panelId); + ctrl.uiSendValue(EditId::UIActivePanel, static_cast(panelId)); setActivePanel(panelId); } break; From 0c0a99c421805d49e3606404665af9261f3d88f6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 10:43:26 +0200 Subject: [PATCH 161/445] Fixes for MinGW --- editor/cmake/Vstgui.cmake | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index e853b091..2cac1f17 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -208,12 +208,21 @@ if(${CMAKE_BUILD_TYPE} MATCHES "Release") target_compile_definitions(sfizz-vstgui PRIVATE "RELEASE") endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + # higher C++ requirement on Windows + set_property(TARGET sfizz-vstgui PROPERTY CXX_STANDARD 14) +endif() + if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(sfizz-vstgui PRIVATE "-Wno-deprecated-copy" + "-Wno-deprecated-declarations" + "-Wno-extra" "-Wno-ignored-qualifiers" + "-Wno-multichar" "-Wno-reorder" "-Wno-sign-compare" + "-Wno-unknown-pragmas" "-Wno-unused-function" "-Wno-unused-parameter" "-Wno-unused-variable") From cf009e61b157bd1538673fca91a5542afb8b1dad Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 11:12:29 +0200 Subject: [PATCH 162/445] More changes for MinGW --- editor/src/editor/utility/vstgui_before.h | 2 ++ lv2/sfizz_ui.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/editor/src/editor/utility/vstgui_before.h b/editor/src/editor/utility/vstgui_before.h index bb01d0c3..8293e82b 100644 --- a/editor/src/editor/utility/vstgui_before.h +++ b/editor/src/editor/utility/vstgui_before.h @@ -9,4 +9,6 @@ #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wignored-qualifiers" #pragma GCC diagnostic ignored "-Wdeprecated-copy" +#pragma GCC diagnostic ignored "-Wmultichar" +#pragma GCC diagnostic ignored "-Wextra" #endif diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 1cbb49a2..69156dea 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -54,6 +54,9 @@ #include "editor/utility/vstgui_before.h" #include "vstgui/lib/cframe.h" #include "vstgui/lib/platform/iplatformframe.h" +#if defined(_WIN32) +#include "vstgui/lib/platform/platform_win32.h" +#endif #include "editor/utility/vstgui_after.h" using namespace VSTGUI; From 15b198d18649aa6987e2ab69df5f757383d538d0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 11:13:00 +0200 Subject: [PATCH 163/445] Eliminate a warning --- lv2/sfizz_ui.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 69156dea..4c4d49ed 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -315,6 +315,8 @@ idle(LV2UI_Handle ui) #if LINUX self->runLoop->execIdle(); +#else + (void)self; #endif return 0; From 66826ef8795b49658fc154a09ad5d763f0593877 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 11:21:26 +0200 Subject: [PATCH 164/445] Rewrite some preprocessor conditionals --- lv2/sfizz_ui.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 4c4d49ed..6baf4021 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -172,9 +172,9 @@ instantiate(const LV2UI_Descriptor *descriptor, // name, and appending "Contents/Resources" (not overridable) // * on Windows, the folder is set programmatically // * on macOS, resource files are looked up using CFBundle APIs -#if _WIN32 +#if defined(_WIN32) IWin32PlatformFrame::setResourceBasePath((std::string(bundle_path) + "\\Contents\\Resources\\").c_str()); -#elif __APPLE__ +#elif defined(__APPLE__) #pragma message("TODO: make resources work on macOS using bundles") #endif From 58fe6fb1156ffddc377f965ee4360aca6a3034d0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 11:38:23 +0200 Subject: [PATCH 165/445] Define DllMain for VSTGUI --- lv2/vstgui_helpers.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lv2/vstgui_helpers.cpp b/lv2/vstgui_helpers.cpp index 4e8969f6..43ff0901 100644 --- a/lv2/vstgui_helpers.cpp +++ b/lv2/vstgui_helpers.cpp @@ -15,6 +15,9 @@ #include #include #endif +#if WINDOWS +#include +#endif #if LINUX void Lv2IdleRunLoop::execIdle() @@ -168,3 +171,16 @@ void VSTGUI::initializeSoHandle() VSTGUI::soHandleInitialized = true; } #endif + +/// +#if WINDOWS +void* hInstance = nullptr; + +__declspec(dllexport) +BOOL WINAPI DllMain(HINSTANCE dllInstance, DWORD reason, LPVOID) +{ + if (reason == DLL_PROCESS_ATTACH) + hInstance = dllInstance; + return TRUE; +} +#endif From 1904aec674e9443737ce2f16d11135b9f066b31e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 12:52:23 +0200 Subject: [PATCH 166/445] Replace absl::variant --- editor/CMakeLists.txt | 2 +- editor/src/editor/EditValue.h | 67 ++++++++++++++++++++++++++++ editor/src/editor/Editor.cpp | 32 ++++++------- editor/src/editor/EditorController.h | 4 +- lv2/sfizz_ui.cpp | 18 ++++---- vst/SfizzVstEditor.cpp | 20 ++++----- 6 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 editor/src/editor/EditValue.h diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 777939d4..65cdb6d9 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -18,4 +18,4 @@ add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/utility/vstgui_before.h) target_include_directories(sfizz_editor PUBLIC "src") target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui) -target_link_libraries(sfizz_editor PUBLIC absl::strings absl::variant) +target_link_libraries(sfizz_editor PUBLIC absl::strings) diff --git a/editor/src/editor/EditValue.h b/editor/src/editor/EditValue.h new file mode 100644 index 00000000..b8d911dd --- /dev/null +++ b/editor/src/editor/EditValue.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include +#include + +class EditValue { +public: + constexpr EditValue() : tag(Nil) {} + EditValue(float value) { reset(value); } + EditValue(std::string value) { reset(value); } + ~EditValue() { reset(); } + + void reset() noexcept + { + if (tag == String) + destruct(u.s); + tag = Nil; + } + + void reset(float value) noexcept + { + reset(); + u.f = value; + tag = Float; + } + + void reset(std::string value) noexcept + { + reset(); + new (&u.s) std::string(std::move(value)); + tag = String; + } + + float to_float() const + { + if (tag != Float) + throw std::runtime_error("the tagged union does not contain `float`"); + return u.f; + } + + const std::string& to_string() const + { + if (tag != String) + throw std::runtime_error("the tagged union does not contain `string`"); + return u.s; + } + +private: + template static void destruct(T& obj) { obj.~T(); } + +private: + enum TypeTag { Nil, Float, String }; + union Union { + constexpr explicit Union(float f = 0.0f) noexcept : f(f) {} + ~Union() noexcept {} + float f; + std::string s; + }; + TypeTag tag { Nil }; + Union u; +}; diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index edad917d..dccae14c 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -158,13 +158,13 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) switch (id) { case EditId::SfzFile: { - const std::string& value = absl::get(v); + const std::string& value = v.to_string(); updateSfzFileLabel(value); } break; case EditId::Volume: { - const float value = absl::get(v); + const float value = v.to_float(); if (volumeSlider_) volumeSlider_->setValue(value); updateVolumeLabel(value); @@ -172,7 +172,7 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::Polyphony: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (numVoicesSlider_) numVoicesSlider_->setValue(value); updateNumVoicesLabel(value); @@ -180,7 +180,7 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::Oversampling: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); int log2Value = 0; for (int f = value; f > 1; f /= 2) @@ -193,7 +193,7 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::PreloadSize: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (preloadSizeSlider_) preloadSizeSlider_->setValue(value); updatePreloadSizeLabel(value); @@ -201,13 +201,13 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::ScalaFile: { - const std::string& value = absl::get(v); + const std::string& value = v.to_string(); updateScalaFileLabel(value); } break; case EditId::ScalaRootKey: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (scalaRootKeySlider_) scalaRootKeySlider_->setValue(value); updateScalaRootKeyLabel(value); @@ -215,7 +215,7 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::TuningFrequency: { - const float value = absl::get(v); + const float value = v.to_float(); if (tuningFrequencySlider_) tuningFrequencySlider_->setValue(value); updateTuningFrequencyLabel(value); @@ -223,7 +223,7 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::StretchTuning: { - const float value = absl::get(v); + const float value = v.to_float(); if (stretchedTuningSlider_) stretchedTuningSlider_->setValue(value); updateStretchedTuningLabel(value); @@ -231,49 +231,49 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::UINumCurves: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (CTextLabel* label = infoCurvesLabel_) formatLabel(label, "%u", value); } break; case EditId::UINumMasters: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (CTextLabel* label = infoMastersLabel_) formatLabel(label, "%u", value); } break; case EditId::UINumGroups: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (CTextLabel* label = infoGroupsLabel_) formatLabel(label, "%u", value); } break; case EditId::UINumRegions: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (CTextLabel* label = infoRegionsLabel_) formatLabel(label, "%u", value); } break; case EditId::UINumPreloadedSamples: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (CTextLabel* label = infoSamplesLabel_) formatLabel(label, "%u", value); } break; case EditId::UINumActiveVoices: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); if (CTextLabel* label = infoVoicesLabel_) formatLabel(label, "%u", value); } break; case EditId::UIActivePanel: { - const int value = static_cast(absl::get(v)); + const int value = static_cast(v.to_float()); setActivePanel(value); } break; diff --git a/editor/src/editor/EditorController.h b/editor/src/editor/EditorController.h index 6b6988c1..4d3db4c9 100644 --- a/editor/src/editor/EditorController.h +++ b/editor/src/editor/EditorController.h @@ -5,11 +5,11 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include "EditValue.h" #include -#include +#include #include enum class EditId : int; -typedef absl::variant EditValue; class EditorController { public: diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 6baf4021..069d4942 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -410,31 +410,31 @@ void sfizz_ui_t::uiSendValue(EditId id, const EditValue& v) switch (id) { case EditId::Volume: - sendFloat(SFIZZ_VOLUME, absl::get(v)); + sendFloat(SFIZZ_VOLUME, v.to_float()); break; case EditId::Polyphony: - sendFloat(SFIZZ_POLYPHONY, absl::get(v)); + sendFloat(SFIZZ_POLYPHONY, v.to_float()); break; case EditId::Oversampling: - sendFloat(SFIZZ_OVERSAMPLING, absl::get(v)); + sendFloat(SFIZZ_OVERSAMPLING, v.to_float()); break; case EditId::PreloadSize: - sendFloat(SFIZZ_PRELOAD, absl::get(v)); + sendFloat(SFIZZ_PRELOAD, v.to_float()); break; case EditId::ScalaRootKey: - sendFloat(SFIZZ_SCALA_ROOT_KEY, absl::get(v)); + sendFloat(SFIZZ_SCALA_ROOT_KEY, v.to_float()); break; case EditId::TuningFrequency: - sendFloat(SFIZZ_TUNING_FREQUENCY, absl::get(v)); + sendFloat(SFIZZ_TUNING_FREQUENCY, v.to_float()); break; case EditId::StretchTuning: - sendFloat(SFIZZ_STRETCH_TUNING, absl::get(v)); + sendFloat(SFIZZ_STRETCH_TUNING, v.to_float()); break; case EditId::SfzFile: - sendPath(sfizz_sfz_file_uri, absl::get(v)); + sendPath(sfizz_sfz_file_uri, v.to_string()); break; case EditId::ScalaFile: - sendPath(sfizz_scala_file_uri, absl::get(v)); + sendPath(sfizz_scala_file_uri, v.to_string()); break; default: break; diff --git a/vst/SfizzVstEditor.cpp b/vst/SfizzVstEditor.cpp index 163034cc..1d0a127f 100644 --- a/vst/SfizzVstEditor.cpp +++ b/vst/SfizzVstEditor.cpp @@ -109,9 +109,9 @@ void SfizzVstEditor::onStateChanged() void SfizzVstEditor::uiSendValue(EditId id, const EditValue& v) { if (id == EditId::SfzFile) - loadSfzFile(absl::get(v)); + loadSfzFile(v.to_string()); else if (id == EditId::ScalaFile) - loadScalaFile(absl::get(v)); + loadScalaFile(v.to_string()); else { SfizzVstController* ctrl = getController(); @@ -123,14 +123,14 @@ void SfizzVstEditor::uiSendValue(EditId id, const EditValue& v) switch (id) { case EditId::Volume: - normalizeAndSet(kPidVolume, kParamVolumeRange, absl::get(v)); + normalizeAndSet(kPidVolume, kParamVolumeRange, v.to_float()); break; case EditId::Polyphony: - normalizeAndSet(kPidNumVoices, kParamNumVoicesRange, absl::get(v)); + normalizeAndSet(kPidNumVoices, kParamNumVoicesRange, v.to_float()); break; case EditId::Oversampling: { - const int32 value = static_cast(absl::get(v)); + const int32 value = static_cast(v.to_float()); int32 log2Value = 0; for (int32 f = value; f > 1; f /= 2) @@ -140,20 +140,20 @@ void SfizzVstEditor::uiSendValue(EditId id, const EditValue& v) } break; case EditId::PreloadSize: - normalizeAndSet(kPidPreloadSize, kParamPreloadSizeRange, absl::get(v)); + normalizeAndSet(kPidPreloadSize, kParamPreloadSizeRange, v.to_float()); break; case EditId::ScalaRootKey: - normalizeAndSet(kPidScalaRootKey, kParamScalaRootKeyRange, absl::get(v)); + normalizeAndSet(kPidScalaRootKey, kParamScalaRootKeyRange, v.to_float()); break; case EditId::TuningFrequency: - normalizeAndSet(kPidTuningFrequency, kParamTuningFrequencyRange, absl::get(v)); + normalizeAndSet(kPidTuningFrequency, kParamTuningFrequencyRange, v.to_float()); break; case EditId::StretchTuning: - normalizeAndSet(kPidStretchedTuning, kParamStretchedTuningRange, absl::get(v)); + normalizeAndSet(kPidStretchedTuning, kParamStretchedTuningRange, v.to_float()); break; case EditId::UIActivePanel: - ctrl->getSfizzUiState().activePanel = static_cast(absl::get(v)); + ctrl->getSfizzUiState().activePanel = static_cast(v.to_float()); break; default: From 1fd771049354cb747fea657596cf209599a0d540 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 13:09:07 +0200 Subject: [PATCH 167/445] Try cmake hack which disables Release/ output dir in MSVC --- appveyor.yml | 3 --- lv2/CMakeLists.txt | 4 ++-- vst/CMakeLists.txt | 13 ++++--------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d5e329a7..ebc8dc20 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,9 +28,6 @@ build_script: - cmd: cmake --build . --config Release -j after_build: -- cmd: cp sfizz.lv2/Release/sfizz.dll sfizz.lv2/ -- cmd: cp sfizz.lv2/Release/sfizz_ui.dll sfizz.lv2/ -- cmd: rm -rf sfizz.lv2/Release - cmd: if %platform%==Win32 set RELEASE_ARCH=x86 - cmd: if %platform%==x64 set RELEASE_ARCH=x64 - cmd: 7z a sfizz-lv2-%APPVEYOR_REPO_TAG_NAME%-%RELEASE_ARCH%-msvc.zip sfizz.lv2 diff --git a/lv2/CMakeLists.txt b/lv2/CMakeLists.txt index c45f964f..3322d3d1 100644 --- a/lv2/CMakeLists.txt +++ b/lv2/CMakeLists.txt @@ -62,12 +62,12 @@ endif() # /lv2/.lv2/. set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES PREFIX "") set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") -set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary") +set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary/$<0:>") if (SFIZZ_LV2_UI) set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES PREFIX "") set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES OUTPUT_NAME "${PROJECT_NAME}_ui") - set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary") + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary/$<0:>") endif() # Generate *.ttl files from *.in sources, diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index f96abd80..c4b3538e 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -75,19 +75,14 @@ endforeach() if(WIN32) set_target_properties(${VSTPLUGIN_PRJ_NAME} PROPERTIES SUFFIX ".vst3" - LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/${VST3_PACKAGE_ARCHITECTURE}-win") - foreach(config ${CMAKE_CONFIGURATION_TYPES}) - string(TOUPPER "${config}" config) - set_target_properties(${VSTPLUGIN_PRJ_NAME} PROPERTIES - "LIBRARY_OUTPUT_DIRECTORY_${config}" "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/${VST3_PACKAGE_ARCHITECTURE}-win") - endforeach() + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/${VST3_PACKAGE_ARCHITECTURE}-win/$<0:>") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/win/Plugin.ico" "${CMAKE_CURRENT_SOURCE_DIR}/win/desktop.ini" DESTINATION "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}") elseif(APPLE) set_target_properties(${VSTPLUGIN_PRJ_NAME} PROPERTIES SUFFIX "" - LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/MacOS") + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/MacOS/$<0:>") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/mac/PkgInfo" DESTINATION "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents") set(SFIZZ_VST3_BUNDLE_EXECUTABLE "${PROJECT_NAME}") @@ -98,7 +93,7 @@ elseif(APPLE) DESTINATION "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") else() set_target_properties(${VSTPLUGIN_PRJ_NAME} PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/${VST3_PACKAGE_ARCHITECTURE}-linux") + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/${VST3_PACKAGE_ARCHITECTURE}-linux/$<0:>") endif() file(COPY "gpl-3.0.txt" @@ -233,7 +228,7 @@ elseif(SFIZZ_AU) COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/${AUPLUGIN_BUNDLE_NAME}/Contents/Resources") set_target_properties(${AUPLUGIN_PRJ_NAME} PROPERTIES SUFFIX "" - LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${AUPLUGIN_BUNDLE_NAME}/Contents/MacOS") + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${AUPLUGIN_BUNDLE_NAME}/Contents/MacOS/$<0:>") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/mac/PkgInfo" DESTINATION "${PROJECT_BINARY_DIR}/${AUPLUGIN_BUNDLE_NAME}/Contents") set(SFIZZ_AU_BUNDLE_EXECUTABLE "${PROJECT_NAME}") From eed54ec8414850143e40195863a71b315e7c02c5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 20:07:46 +0200 Subject: [PATCH 168/445] Correct a mistake in french text --- lv2/sfizz.ttl.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index f4090158..cddda959 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -362,7 +362,7 @@ midnam:update a lv2:Feature . lv2:index 16 ; lv2:symbol "num_regions" ; lv2:name "Number of regions", - "Nombre de regions"@fr ; + "Nombre de régions"@fr ; pg:group <@LV2PLUGIN_URI@#status> ; lv2:portProperty lv2:integer ; lv2:default 0 ; From 166be3c84398e596d0f6fd4f153ebe4d8b4429dc Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 20:16:48 +0200 Subject: [PATCH 169/445] Update innosetup --- scripts/innosetup.iss.in | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/innosetup.iss.in b/scripts/innosetup.iss.in index d97d3f2b..0b4aa85a 100644 --- a/scripts/innosetup.iss.in +++ b/scripts/innosetup.iss.in @@ -49,6 +49,7 @@ Name: "vst3"; Description: "VST3 plugin"; Types: full custom; [Files] Source: "sfizz.lv2\sfizz.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2"; Flags: ignoreversion +Source: "sfizz.lv2\sfizz_ui.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2"; Flags: ignoreversion Source: "sfizz.lv2\manifest.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\sfizz.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\lgpl-3.0.txt"; Components: main; DestDir: "{app}" From 7417cd23ab2a35249d3646b436f0f219227ea29b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 20:32:58 +0200 Subject: [PATCH 170/445] Attempt to fix LV2 Vstgui on macOS --- lv2/sfizz_ui.cpp | 3 +++ lv2/vstgui_helpers.cpp | 38 ++++++++++++++++++++++++++++++++++++++ lv2/vstgui_helpers.h | 7 +++++++ 3 files changed, 48 insertions(+) diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 069d4942..b7fe308b 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -374,6 +374,9 @@ lv2ui_descriptor(uint32_t index) #if LINUX VSTGUI::initializeSoHandle(); #endif +#if MAC + VSTGUI::initializeBundleRef(); +#endif switch (index) { diff --git a/lv2/vstgui_helpers.cpp b/lv2/vstgui_helpers.cpp index 43ff0901..d1ad3187 100644 --- a/lv2/vstgui_helpers.cpp +++ b/lv2/vstgui_helpers.cpp @@ -18,6 +18,9 @@ #if WINDOWS #include #endif +#if MAC +#include "vstgui/plugin-bindings/getpluginbundle.h" +#endif #if LINUX void Lv2IdleRunLoop::execIdle() @@ -184,3 +187,38 @@ BOOL WINAPI DllMain(HINSTANCE dllInstance, DWORD reason, LPVOID) return TRUE; } #endif + +/// +#if MAC +namespace VSTGUI +{ +void* gBundleRef = nullptr; + +static volatile bool gBundleRefInitialized = false; +static std::mutex gBundleRefMutex; + +struct CFBundle_deleter { + void operator()(CFBundleRef x) const noexcept + { + CFRelease(x); + } +}; +static std::unique_ptr<__CFBundle, CFBundle_deleter> gBundleRefPointer; +} // namespace VSTGUI + +void VSTGUI::initializeBundleRef() +{ + if (VSTGUI::gBundleRefInitialized) + return; + + std::lock_guard lock(VSTGUI::gBundleRefMutex); + if (VSTGUI::gBundleRefInitialized) + return; + + CFBundleRef bundleRef = GetPluginBundle(); + VSTGUI::gBundleRef = bundleRef; + VSTGUI::gBundleRefPointer.reset(bundleRef); + + VSTGUI::gBundleRefInitialized = true; +} +#endif diff --git a/lv2/vstgui_helpers.h b/lv2/vstgui_helpers.h index bb48936e..8367a2c4 100644 --- a/lv2/vstgui_helpers.h +++ b/lv2/vstgui_helpers.h @@ -66,3 +66,10 @@ namespace VSTGUI void initializeSoHandle(); } #endif + +#if MAC +namespace VSTGUI +{ +void initializeBundleRef(); +} +#endif From 833606aef9f2e75a50d56a728738c9a17261a958 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 20:38:56 +0200 Subject: [PATCH 171/445] Update innosetup --- scripts/innosetup.iss.in | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/innosetup.iss.in b/scripts/innosetup.iss.in index 0b4aa85a..b287de6a 100644 --- a/scripts/innosetup.iss.in +++ b/scripts/innosetup.iss.in @@ -48,10 +48,14 @@ Name: "lv2"; Description: "LV2 plugin"; Types: full custom; Name: "vst3"; Description: "VST3 plugin"; Types: full custom; [Files] -Source: "sfizz.lv2\sfizz.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2"; Flags: ignoreversion -Source: "sfizz.lv2\sfizz_ui.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2"; Flags: ignoreversion +Source: "sfizz.lv2\Contents\Binary\sfizz.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Binary"; Flags: ignoreversion +Source: "sfizz.lv2\Contents\Binary\sfizz_ui.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Binary"; Flags: ignoreversion +Source: "sfizz.lv2\Contents\Resources\DefaultInstrument.sfz"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" +Source: "sfizz.lv2\Contents\Resources\DefaultScale.scl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" +Source: "sfizz.lv2\Contents\Resources\logo.png"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" Source: "sfizz.lv2\manifest.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\sfizz.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" +Source: "sfizz.lv2\sfizz_ui.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\lgpl-3.0.txt"; Components: main; DestDir: "{app}" Source: "sfizz.lv2\LICENSE.md"; Components: main; DestDir: "{app}" Source: "sfizz.vst3\desktop.ini"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3" From d008fb3bc8677188b9a60344b7020e7f462d77db Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 23:15:54 +0200 Subject: [PATCH 172/445] Bundle checked to work on macOS, remove message [ci skip] --- lv2/sfizz_ui.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index b7fe308b..020f5989 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -174,8 +174,6 @@ instantiate(const LV2UI_Descriptor *descriptor, // * on macOS, resource files are looked up using CFBundle APIs #if defined(_WIN32) IWin32PlatformFrame::setResourceBasePath((std::string(bundle_path) + "\\Contents\\Resources\\").c_str()); -#elif defined(__APPLE__) - #pragma message("TODO: make resources work on macOS using bundles") #endif // makes labels refresh correctly From c27b3cb192406d5d26507c875a5da046190a089f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 2 Sep 2020 10:22:49 +0200 Subject: [PATCH 173/445] Set draw mode in the slider painter --- editor/src/editor/GUIComponents.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index 56fb6f8e..a85bf64b 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -28,6 +28,8 @@ void SimpleSlider::draw(CDrawContext* dc) CRect bounds = getViewSize(); CRect handle = calculateHandleRect(getValueNormalized()); + dc->setDrawMode(kAntiAliasing); + dc->setFrameColor(_frame); dc->drawRect(bounds, kDrawStroked); From 1ef7631cc8c51ccee5e73f75e9f7a169e33414b6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 2 Sep 2020 11:23:50 +0200 Subject: [PATCH 174/445] Update to custom Vstgui which fixes drawing issues --- editor/external/vstgui4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index c6a7f607..ecb6892f 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit c6a7f607c21a7353e922a6d45e54d6c56d5a6745 +Subproject commit ecb6892fea8a1aa7671376b6306724f8fababc1a From 239195ae74abad0689b902b26f7753591b06b95b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 2 Sep 2020 14:06:25 +0200 Subject: [PATCH 175/445] Update of vstgui to resolve a coordinate problem --- editor/external/vstgui4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index ecb6892f..8390d355 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit ecb6892fea8a1aa7671376b6306724f8fababc1a +Subproject commit 8390d355fb65c6426d22a97de16a39a1be673481 From 026875ae98e50d2ab898c6053062992135c3b845 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 4 Sep 2020 07:57:14 +0200 Subject: [PATCH 176/445] Update vstgui to fix cairo font matching --- editor/external/vstgui4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index 8390d355..7316f9fb 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit 8390d355fb65c6426d22a97de16a39a1be673481 +Subproject commit 7316f9fb2891b9f0e2a408cfbf4ec35aeec268bd From 174194ebd2df2795b516bee906b04efe0c5f59bc Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 4 Sep 2020 10:30:03 +0200 Subject: [PATCH 177/445] Fix a plugin crash on closing UI --- lv2/sfizz_ui.cpp | 13 +++--- lv2/vstgui_helpers.cpp | 90 +++++++++++++++++++----------------------- lv2/vstgui_helpers.h | 18 ++++++++- 3 files changed, 63 insertions(+), 58 deletions(-) diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 020f5989..10ed2cbe 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -74,6 +74,12 @@ typedef std::unique_ptr FrameHolder; /// struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { +#if LINUX + SoHandleInitializer soHandleInitializer; +#endif +#if MAC + BundleRefInitializer bundleRefInitializer; +#endif LV2UI_Write_Function write = nullptr; LV2UI_Controller con = nullptr; LV2_URID_Map *map = nullptr; @@ -369,13 +375,6 @@ LV2_SYMBOL_EXPORT const LV2UI_Descriptor * lv2ui_descriptor(uint32_t index) { -#if LINUX - VSTGUI::initializeSoHandle(); -#endif -#if MAC - VSTGUI::initializeBundleRef(); -#endif - switch (index) { case 0: diff --git a/lv2/vstgui_helpers.cpp b/lv2/vstgui_helpers.cpp index d1ad3187..0a402a20 100644 --- a/lv2/vstgui_helpers.cpp +++ b/lv2/vstgui_helpers.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #if LINUX #include #include @@ -143,36 +144,31 @@ namespace VSTGUI { void* soHandle = nullptr; -static volatile bool soHandleInitialized = false; +static volatile size_t soHandleCount = 0; static std::mutex soHandleMutex; -struct Dl_handle_deleter +SoHandleInitializer::SoHandleInitializer() { - void operator()(void* x) const noexcept - { - dlclose(x); - } -}; -static std::unique_ptr soHandlePointer; -} // namespace VSTGUI - -void VSTGUI::initializeSoHandle() -{ - if (VSTGUI::soHandleInitialized) - return; - - std::lock_guard lock(VSTGUI::soHandleMutex); - if (VSTGUI::soHandleInitialized) - return; - - Dl_info info; - if (dladdr((void*)&lv2ui_descriptor, &info)) - { - VSTGUI::soHandle = dlopen(info.dli_fname, RTLD_LAZY); - VSTGUI::soHandlePointer.reset(VSTGUI::soHandle); - } - VSTGUI::soHandleInitialized = true; + std::lock_guard lock(soHandleMutex); + if (soHandleCount++ == 0) { + Dl_info info; + if (dladdr((void*)&lv2ui_descriptor, &info)) + soHandle = dlopen(info.dli_fname, RTLD_LAZY); + if (!soHandle) + throw std::runtime_error("SoHandleInitializer"); + } } + +SoHandleInitializer::~SoHandleInitializer() +{ + std::lock_guard lock(soHandleMutex); + if (--soHandleCount == 0) { + dlclose(soHandle); + soHandle = nullptr; + } +} + +} // namespace VSTGUI #endif /// @@ -194,31 +190,27 @@ namespace VSTGUI { void* gBundleRef = nullptr; -static volatile bool gBundleRefInitialized = false; +static volatile size_t gBundleRefCount = 0; static std::mutex gBundleRefMutex; -struct CFBundle_deleter { - void operator()(CFBundleRef x) const noexcept - { - CFRelease(x); - } -}; -static std::unique_ptr<__CFBundle, CFBundle_deleter> gBundleRefPointer; -} // namespace VSTGUI - -void VSTGUI::initializeBundleRef() +BundleRefInitializer::BundleRefInitializer() { - if (VSTGUI::gBundleRefInitialized) - return; - - std::lock_guard lock(VSTGUI::gBundleRefMutex); - if (VSTGUI::gBundleRefInitialized) - return; - - CFBundleRef bundleRef = GetPluginBundle(); - VSTGUI::gBundleRef = bundleRef; - VSTGUI::gBundleRefPointer.reset(bundleRef); - - VSTGUI::gBundleRefInitialized = true; + std::lock_guard lock(gBundleRefMutex); + if (bundleRefCount++ == 0) { + gBundleRef = GetPluginBundle(); + if (!gBundleRef) + throw std::runtime_error("BundleRefInitializer"); + } } + +BundleRefInitializer::~BundleRefInitializer() +{ + std::lock_guard lock(gBundleRefMutex); + if (--bundleRefCount == 0) { + CFRelease((CFBundleRef)gBundleRef); + gBundleRef = nullptr; + } +} + +} // namespace VSTGUI #endif diff --git a/lv2/vstgui_helpers.h b/lv2/vstgui_helpers.h index 8367a2c4..2fbcd841 100644 --- a/lv2/vstgui_helpers.h +++ b/lv2/vstgui_helpers.h @@ -63,13 +63,27 @@ private: #if LINUX namespace VSTGUI { -void initializeSoHandle(); +class SoHandleInitializer { +public: + SoHandleInitializer(); + ~SoHandleInitializer(); +private: + SoHandleInitializer(const SoHandleInitializer&) = delete; + SoHandleInitializer& operator=(const SoHandleInitializer&) = delete; +}; } #endif #if MAC namespace VSTGUI { -void initializeBundleRef(); +class BundleRefInitializer { +public: + BundleRefInitializer(); + ~BundleRefInitializer(); +private: + BundleRefInitializer(const BundleRefInitializer&) = delete; + BundleRefInitializer& operator=(const BundleRefInitializer&) = delete; +}; } #endif From 2512d8047fe1f498aec0b518bbc0d5cf5e60d23f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 4 Sep 2020 11:04:04 +0200 Subject: [PATCH 178/445] Try to repair the macOS build --- lv2/vstgui_helpers.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lv2/vstgui_helpers.cpp b/lv2/vstgui_helpers.cpp index 0a402a20..31637bb0 100644 --- a/lv2/vstgui_helpers.cpp +++ b/lv2/vstgui_helpers.cpp @@ -196,7 +196,7 @@ static std::mutex gBundleRefMutex; BundleRefInitializer::BundleRefInitializer() { std::lock_guard lock(gBundleRefMutex); - if (bundleRefCount++ == 0) { + if (gBundleRefCount++ == 0) { gBundleRef = GetPluginBundle(); if (!gBundleRef) throw std::runtime_error("BundleRefInitializer"); @@ -206,7 +206,7 @@ BundleRefInitializer::BundleRefInitializer() BundleRefInitializer::~BundleRefInitializer() { std::lock_guard lock(gBundleRefMutex); - if (--bundleRefCount == 0) { + if (--gBundleRefCount == 0) { CFRelease((CFBundleRef)gBundleRef); gBundleRef = nullptr; } From 0205aca3751f95945a6fcbc3edac022c8454b227 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 5 Sep 2020 01:23:10 +0200 Subject: [PATCH 179/445] vstgui: fix various ownership problems --- editor/src/editor/Editor.cpp | 12 +++++++----- lv2/sfizz_ui.cpp | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index dccae14c..d50aa5b4 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -131,6 +131,8 @@ Editor::~Editor() { Impl& impl = *impl_; + close(); + EditorController& ctrl = *impl.ctrl_; ctrl.decorate(nullptr); } @@ -148,7 +150,7 @@ void Editor::close() Impl& impl = *impl_; if (impl.frame_) { - impl.frame_->removeView(impl.view_.get()); + impl.frame_->removeView(impl.view_.get(), false); impl.frame_ = nullptr; } } @@ -284,11 +286,11 @@ void Editor::Impl::createFrameContents() { const CRect bounds { 0.0, 0.0, static_cast(viewWidth), static_cast(viewHeight) }; CViewContainer* view = new CViewContainer(bounds); - view_ = view; + view_ = owned(view); view->setBackgroundColor(CColor(0xff, 0xff, 0xff)); - SharedPointer logo { new CBitmap("logo.png") }; + SharedPointer logo = owned(new CBitmap("logo.png")); CRect bottomRow = bounds; bottomRow.top = bottomRow.bottom - 30; @@ -688,7 +690,7 @@ void Editor::Impl::createFrameContents() void Editor::Impl::chooseSfzFile() { - SharedPointer fs(CNewFileSelector::create(frame_)); + SharedPointer fs = owned(CNewFileSelector::create(frame_)); fs->setTitle("Load SFZ file"); fs->setDefaultExtension(CFileExtension("SFZ", "sfz")); @@ -705,7 +707,7 @@ void Editor::Impl::chooseSfzFile() void Editor::Impl::chooseScalaFile() { - SharedPointer fs(CNewFileSelector::create(frame_)); + SharedPointer fs = owned(CNewFileSelector::create(frame_)); fs->setTitle("Load Scala file"); fs->setDefaultExtension(CFileExtension("SCL", "scl")); diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 10ed2cbe..70ff76e1 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -86,8 +86,8 @@ struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { LV2_URID_Unmap *unmap = nullptr; LV2UI_Resize *resize = nullptr; LV2UI_Touch *touch = nullptr; - std::unique_ptr editor; FrameHolder uiFrame; + std::unique_ptr editor; #if LINUX SharedPointer runLoop; #endif @@ -191,7 +191,7 @@ instantiate(const LV2UI_Descriptor *descriptor, IPlatformFrameConfig* config = nullptr; #if LINUX - SharedPointer runLoop = new Lv2IdleRunLoop; + SharedPointer runLoop = owned(new Lv2IdleRunLoop); self->runLoop = runLoop; VSTGUI::X11::FrameConfig x11Config; x11Config.runLoop = runLoop; From 0849a1b5e560b295f238abe7ae0b6f916c65d762 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 5 Sep 2020 07:03:57 +0200 Subject: [PATCH 180/445] Set the deployment target for macOS --- cmake/SfizzConfig.cmake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index ae6aec60..270954e8 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -17,6 +17,11 @@ if (WIN32) add_compile_definitions(_WIN32_WINNT=0x601) endif() +# Set macOS compatibility level +if (APPLE) + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") +endif() + # Do not define macros `min` and `max` if (WIN32) add_compile_definitions(NOMINMAX) From 19469c5c1fc60255333804cd0e9af6d8e669cf17 Mon Sep 17 00:00:00 2001 From: redtide Date: Sat, 5 Sep 2020 16:14:51 +0200 Subject: [PATCH 181/445] Doxygen documentation update [skip ci] --- doxygen/scripts/Doxyfile.in | 13 +- src/sfizz.h | 604 +++++++++++++++++++++--------------- src/sfizz.hpp | 199 +++++++----- 3 files changed, 483 insertions(+), 333 deletions(-) diff --git a/doxygen/scripts/Doxyfile.in b/doxygen/scripts/Doxyfile.in index 0f7eb945..0465a14e 100644 --- a/doxygen/scripts/Doxyfile.in +++ b/doxygen/scripts/Doxyfile.in @@ -252,13 +252,8 @@ TAB_SIZE = 4 # a double escape (\\{ and \\}) ALIASES = "true=true" \ - "false=false" - -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = + "false=false" \ + "null=NULL # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For @@ -892,7 +887,7 @@ EXCLUDE_PATTERNS = # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories use the pattern */test/* -EXCLUDE_SYMBOLS = +EXCLUDE_SYMBOLS = SFIZZ_EXPORTED_API # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include @@ -1103,7 +1098,7 @@ GENERATE_HTML = YES # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_OUTPUT = _api +HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). diff --git a/src/sfizz.h b/src/sfizz.h index 5c8f7d1a..5b2ea170 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -6,7 +6,7 @@ /** @file - @brief sfizz public C API + @brief sfizz public C API. */ #pragma once @@ -28,11 +28,14 @@ extern "C" { #endif /** - * @brief Synth handle + * @brief Synth handle. + * @since 0.2.0 */ typedef struct sfizz_synth_t sfizz_synth_t; + /** * @brief Oversampling factor + * @since 0.2.0 */ typedef enum { SFIZZ_OVERSAMPLING_X1 = 1, @@ -40,8 +43,10 @@ typedef enum { SFIZZ_OVERSAMPLING_X4 = 4, SFIZZ_OVERSAMPLING_X8 = 8 } sfizz_oversampling_factor_t; + /** * @brief Processing mode + * @since 0.4.1 */ typedef enum { SFIZZ_PROCESS_LIVE, @@ -49,47 +54,52 @@ typedef enum { } sfizz_process_mode_t; /** - * @brief Creates a sfizz synth. This object has to be freed by the caller - * using sfizz_free(). The synth by default is set at 48 kHz - * and a maximum block size of 1024. You should change these values - * if they are not correct for your application. + * @brief Creates a sfizz synth. + * + * This object has to be freed by the caller using sfizz_free(). + * The synth by default is set at 48 kHz and a maximum block size of 1024. + * You should change these values if they are not correct for your application. + * @since 0.2.0 */ SFIZZ_EXPORTED_API sfizz_synth_t* sfizz_create_synth(); /** - * @brief Frees an existing sfizz synth. + * @brief Frees an existing sfizz synth. + * @since 0.2.0 * - * @param synth The synth to destroy. + * @param synth The synth to destroy. */ SFIZZ_EXPORTED_API void sfizz_free(sfizz_synth_t* synth); /** - * @brief Loads an SFZ file. The file path can be absolute or relative. All - * file operations for this SFZ file will be relative to the parent - * directory of the SFZ file. + * @brief Loads an SFZ file. * - * @param synth The sfizz synth. - * @param path A null-terminated string representing a path to an SFZ - * file. + * The file path can be absolute or relative. All file operations for this SFZ + * file will be relative to the parent directory of the SFZ file. + * @since 0.2.0 * - * @return @true when file loading went OK, - * @false if some error occured while loading. + * @param synth The synth. + * @param path A null-terminated string representing a path to an SFZ file. + * + * @return @true when file loading went OK, + * @false if some error occured while loading. */ SFIZZ_EXPORTED_API bool sfizz_load_file(sfizz_synth_t* synth, const char* path); /** - * @brief Loads an SFZ file from textual data. This accepts a virtual - * path name for the imaginary sfz file, which is not required to - * exist on disk. The purpose of the virtual path is to locate - * samples with relative paths. + * @brief Loads an SFZ file from textual data. + * + * This accepts a virtual path name for the imaginary sfz file, which is not + * required to exist on disk. The purpose of the virtual path is to locate + * samples with relative paths. * @since 0.4.0 * - * @param synth The sfizz synth. - * @param path The virtual path of the SFZ file. - * @param text The contents of the virtual SFZ file. + * @param synth The synth. + * @param path The virtual path of the SFZ file. + * @param text The contents of the virtual SFZ file. * - * @return @true when file loading went OK, - * @false if some error occured while loading. + * @return @true when file loading went OK, + * @false if some error occured while loading. */ SFIZZ_EXPORTED_API bool sfizz_load_string(sfizz_synth_t* synth, const char* path, const char* text); @@ -97,10 +107,11 @@ SFIZZ_EXPORTED_API bool sfizz_load_string(sfizz_synth_t* synth, const char* path * @brief Sets the tuning from a Scala file loaded from the file system. * @since 0.4.0 * - * @param synth The sfizz synth. - * @param path The path to the file in Scala format. - * @return @true when tuning scale loaded OK, - * @false if some error occurred. + * @param synth The synth. + * @param path The path to the file in Scala format. + * + * @return @true when tuning scale loaded OK, + * @false if some error occurred. */ SFIZZ_EXPORTED_API bool sfizz_load_scala_file(sfizz_synth_t* synth, const char* path); @@ -108,10 +119,11 @@ SFIZZ_EXPORTED_API bool sfizz_load_scala_file(sfizz_synth_t* synth, const char* * @brief Sets the tuning from a Scala file loaded from memory. * @since 0.4.0 * - * @param synth The sfizz synth. - * @param text The contents of the file in Scala format. - * @return @true when tuning scale loaded OK, - * @false if some error occurred. + * @param synth The synth. + * @param text The contents of the file in Scala format. + * + * @return @true when tuning scale loaded OK, + * @false if some error occurred. */ SFIZZ_EXPORTED_API bool sfizz_load_scala_string(sfizz_synth_t* synth, const char* text); @@ -119,8 +131,8 @@ SFIZZ_EXPORTED_API bool sfizz_load_scala_string(sfizz_synth_t* synth, const char * @brief Sets the scala root key. * @since 0.4.0 * - * @param synth The sfizz synth. - * @param root_key The MIDI number of the Scala root key (default 60 for C4). + * @param synth The synth. + * @param root_key The MIDI number of the Scala root key (default 60 for C4). */ SFIZZ_EXPORTED_API void sfizz_set_scala_root_key(sfizz_synth_t* synth, int root_key); @@ -128,8 +140,9 @@ SFIZZ_EXPORTED_API void sfizz_set_scala_root_key(sfizz_synth_t* synth, int root_ * @brief Gets the scala root key. * @since 0.4.0 * - * @param synth The sfizz synth. - * @return The MIDI number of the Scala root key (default 60 for C4). + * @param synth The synth. + * + * @return The MIDI number of the Scala root key (default 60 for C4). */ SFIZZ_EXPORTED_API int sfizz_get_scala_root_key(sfizz_synth_t* synth); @@ -137,8 +150,8 @@ SFIZZ_EXPORTED_API int sfizz_get_scala_root_key(sfizz_synth_t* synth); * @brief Sets the reference tuning frequency. * @since 0.4.0 * - * @param synth The sfizz synth. - * @param frequency The frequency which indicates where standard tuning A4 is (default 440 Hz). + * @param synth The synth. + * @param frequency The frequency which indicates where standard tuning A4 is (default 440 Hz). */ SFIZZ_EXPORTED_API void sfizz_set_tuning_frequency(sfizz_synth_t* synth, float frequency); @@ -146,231 +159,269 @@ SFIZZ_EXPORTED_API void sfizz_set_tuning_frequency(sfizz_synth_t* synth, float f * @brief Gets the reference tuning frequency. * @since 0.4.0 * - * @param synth The sfizz synth. - * @return The frequency which indicates where standard tuning A4 is (default 440 Hz). + * @param synth The synth. + * + * @return The frequency which indicates where standard tuning A4 is (default 440 Hz). */ SFIZZ_EXPORTED_API float sfizz_get_tuning_frequency(sfizz_synth_t* synth); /** - * @brief Configure stretch tuning using a predefined parametric Railsback curve. - * A ratio 1/2 is supposed to match the average piano; 0 disables (the default). + * @brief Configure stretch tuning using a predefined parametric Railsback curve. + * + * A ratio 1/2 is supposed to match the average piano; 0 disables (the default). * @since 0.4.0 * - * @param synth The sfizz synth. - * @param ratio The parameter in domain 0-1. + * @param synth The synth. + * @param ratio The parameter in domain 0-1. */ SFIZZ_EXPORTED_API void sfizz_load_stretch_tuning_by_ratio(sfizz_synth_t* synth, float ratio); /** - * @brief Return the number of regions in the currently loaded SFZ file. + * @brief Return the number of regions in the currently loaded SFZ file. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_regions(sfizz_synth_t* synth); + /** - * @brief Return the number of groups in the currently loaded SFZ file. + * @brief Return the number of groups in the currently loaded SFZ file. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_groups(sfizz_synth_t* synth); + /** - * @brief Return the number of masters in the currently loaded SFZ file. + * @brief Return the number of masters in the currently loaded SFZ file. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_masters(sfizz_synth_t* synth); + /** - * @brief Return the number of curves in the currently loaded SFZ file. + * @brief Return the number of curves in the currently loaded SFZ file. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_curves(sfizz_synth_t* synth); + /** - * @brief Export a MIDI Name document describing the the currently loaded - * SFZ file. + * @brief Export a MIDI Name document describing the currently loaded SFZ file. + * @since 0.3.1 * - * @param synth The synth. - * @param model The model name used if a non-empty string, otherwise generated. + * @param synth The synth. + * @param model The model name used if a non-empty string, otherwise generated. * - * @return A newly allocated XML string, which must be freed after use. + * @return A newly allocated XML string, which must be freed after use. */ SFIZZ_EXPORTED_API char* sfizz_export_midnam(sfizz_synth_t* synth, const char* model); + /** - * @brief Return the number of preloaded samples for the current SFZ file. + * @brief Return the number of preloaded samples for the current SFZ file. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API size_t sfizz_get_num_preloaded_samples(sfizz_synth_t* synth); + /** - * @brief Return the number of active voices. Note that this function is a - * basic indicator and does not aim to be perfect. In particular, it - * runs on the calling thread so voices may well start or stop while - * the function is checking which voice is active. + * @brief Return the number of active voices. * - * @param synth The synth. + * Note that this function is a basic indicator and does not aim to be perfect. + * In particular, it runs on the calling thread so voices may well start or stop + * while the function is checking which voice is active. + * @since 0.2.0 + * + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_active_voices(sfizz_synth_t* synth); /** - * @brief Set the expected number of samples per block. If unsure, give an - * upper bound since right now ugly things may happen if you go over - * this number. + * @brief Set the expected number of samples per block. * - * @param synth The synth. - * @param samples_per_block The number of samples per block. + * If unsure, give an upper bound since right now ugly things may happen if you + * go over this number. + * @since 0.2.0 + * + * @param synth The synth. + * @param samples_per_block The number of samples per block. */ SFIZZ_EXPORTED_API void sfizz_set_samples_per_block(sfizz_synth_t* synth, int samples_per_block); + /** - * @brief Set the sample rate for the synth. This is the output sample - * rate. This setting does not affect the internal processing. + * @brief Set the sample rate for the synth. * - * @param synth The synth - * @param sample_rate The sample rate. + * This is the output sample rate. This setting does not affect the internal processing. + * @since 0.2.0 + * + * @param synth The synth + * @param sample_rate The sample rate. */ SFIZZ_EXPORTED_API void sfizz_set_sample_rate(sfizz_synth_t* synth, float sample_rate); /** - * @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. + * @brief Send a note on event to the synth. * - * @param synth The synth. - * @param delay The delay of the event in the block, in samples. - * @param note_number The MIDI note number. - * @param velocity The MIDI velocity. + * 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 + * + * @param synth The synth. + * @param delay The delay of the event in the block, in samples. + * @param note_number The MIDI note number. + * @param velocity The MIDI velocity. */ SFIZZ_EXPORTED_API void sfizz_send_note_on(sfizz_synth_t* synth, int delay, int note_number, char velocity); /** - * @brief Send a note off 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. - * As per the SFZ spec the velocity of note-off events is usually replaced by - * the note-on velocity. + * @brief Send a note off event to the synth. * - * @param synth The synth. - * @param delay The delay of the event in the block, in samples. - * @param note_number The MIDI note number. - * @param velocity The MIDI velocity. + * 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. + * @param note_number The MIDI note number. + * @param velocity The MIDI velocity. */ SFIZZ_EXPORTED_API void sfizz_send_note_off(sfizz_synth_t* synth, int delay, int note_number, char velocity); /** - * @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. + * @brief Send a CC event to the synth. * - * @param synth The synth. - * @param delay The delay of the event in the block, in samples. - * @param cc_number The MIDI CC number. - * @param cc_value The MIDI CC value. + * 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 + * + * @param synth The synth. + * @param delay The delay of the event in the block, in samples. + * @param cc_number The MIDI CC number. + * @param cc_value The MIDI CC value. */ SFIZZ_EXPORTED_API void sfizz_send_cc(sfizz_synth_t* synth, int delay, int cc_number, char cc_value); /** - * @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. + * @brief Send a high precision CC event to the synth. * - * @param synth The synth. - * @param delay The delay of the event in the block, in samples. - * @param cc_number The MIDI CC number. - * @param norm_value The normalized CC value, in domain 0 to 1. + * 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 + * + * @param synth The synth. + * @param delay The delay of the event in the block, in samples. + * @param cc_number The MIDI CC number. + * @param norm_value The normalized CC value, in domain 0 to 1. */ SFIZZ_EXPORTED_API void sfizz_send_hdcc(sfizz_synth_t* synth, int delay, int cc_number, float norm_value); /** - * @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. + * @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 * - * @param synth The synth. - * @param delay The delay. - * @param pitch The pitch. + * @param synth The synth. + * @param delay The delay. + * @param pitch The pitch. */ SFIZZ_EXPORTED_API void sfizz_send_pitch_wheel(sfizz_synth_t* synth, int delay, int pitch); /** - * @brief Send an aftertouch event. (CURRENTLY UNIMPLEMENTED) + * @brief Send an aftertouch event. (CURRENTLY UNIMPLEMENTED) + * @since 0.2.0 * - * @param synth - * @param delay - * @param aftertouch + * @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(). + * @param aftertouch The aftertouch value. */ SFIZZ_EXPORTED_API void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, char aftertouch); /** - * @brief Send a tempo event. + * @brief Send a tempo event. + * @since 0.2.0 * - * @param synth The synth. - * @param delay The delay. - * @param seconds_per_beat The seconds per beat. + * @param synth The synth. + * @param delay The delay. + * @param seconds_per_beat The seconds per beat. */ SFIZZ_EXPORTED_API void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float seconds_per_beat); /** - * @brief Send the time signature. + * @brief Send the time signature. + * @since 0.4.1 * - * @param synth The synth. - * @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 synth The synth. + * @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. */ SFIZZ_EXPORTED_API void sfizz_send_time_signature(sfizz_synth_t* synth, int delay, int beats_per_bar, int beat_unit); /** - * @brief Send the time position. + * @brief Send the time position. + * @since 0.4.1 * - * @param synth The synth. - * @param delay The delay. - * @param bar The current bar. - * @param bar_beat The fractional position of the current beat within the bar. + * @param synth The synth. + * @param delay The delay. + * @param bar The current bar. + * @param bar_beat The fractional position of the current beat within the bar. */ SFIZZ_EXPORTED_API void sfizz_send_time_position(sfizz_synth_t* synth, int delay, int bar, float bar_beat); /** - * @brief Send the playback state. + * @brief Send the playback state. + * @since 0.4.1 * - * @param synth The synth. - * @param delay The delay. - * @param playback_state The playback state, 1 if playing, 0 if stopped. + * @param synth The synth. + * @param delay The delay. + * @param playback_state The playback state, 1 if playing, 0 if stopped. */ SFIZZ_EXPORTED_API void sfizz_send_playback_state(sfizz_synth_t* synth, int delay, int playback_state); - /** - * @brief Render a block audio data into a stereo channel. No other channel - * configuration is supported. The synth will gracefully ignore your - * request if you provide a value. You should pass all the relevant - * events 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. + * @brief Render a block audio data into a stereo channel. * - * @param synth The synth. - * @param channels Pointers to the left and right channel of the - * output. - * @param num_channels Should be equal to 2 for the time being. - * @param num_frames Number of frames to fill. This should be less than - * or equal to the expected samples_per_block. + * No other channel configuration is supported. The synth will gracefully ignore + * your request if you provide a value. You should pass all the relevant events + * 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. + * @param channels Pointers to the left and right channel of the output. + * @param num_channels Should be equal to 2 for the time being. + * @param num_frames Number of frames to fill. This should be less than + * or equal to the expected samples_per_block. */ SFIZZ_EXPORTED_API void sfizz_render_block(sfizz_synth_t* synth, float** channels, int num_channels, int num_frames); /** - * @brief Get the size of the preloaded data. This returns the number of - * floats used in the preloading buffers. + * @brief Get the size of the preloaded data. * - * @param synth The synth. + * This returns the number of floats used in the preloading buffers. + * @since 0.2.0 + * + * @param synth The synth. */ SFIZZ_EXPORTED_API unsigned int sfizz_get_preload_size(sfizz_synth_t* synth); + /** - * @brief Set the size of the preloaded data in number of floats (not - * bytes). This will disable the callbacks for the duration of the - * load. This function takes a lock ; prefer calling - * it out of the RT thread. It can also take a long time to return. - * If the new preload size is the same as the current one, it will - * release the lock immediately and exit. + * @brief Set the size of the preloaded data in number of floats (not bytes). + * + * This will disable the callbacks for the duration of the load. + * This function takes a lock ; prefer calling it out of the RT thread. + * It can also take a long time to return. If the new preload size is the same + * as the current one, it will release the lock immediately and exit. + * @since 0.2.0 * * @param synth The synth. * @param[in] preload_size The preload size. @@ -378,199 +429,231 @@ SFIZZ_EXPORTED_API unsigned int sfizz_get_preload_size(sfizz_synth_t* synth); SFIZZ_EXPORTED_API void sfizz_set_preload_size(sfizz_synth_t* synth, unsigned int preload_size); /** - * @brief Get the internal oversampling rate. This is the sampling rate of - * the engine, not the output or expected rate of the calling - * function. For the latter use the `get_sample_rate()` functions. + * @brief Get the internal oversampling rate. * - * @param synth The synth. + * This is the sampling rate of the engine, not the output or expected rate of + * the calling function. For the latter use the sfizz_get_sample_rate() function. + * @since 0.2.0 + * + * @param synth The synth. */ SFIZZ_EXPORTED_API sfizz_oversampling_factor_t sfizz_get_oversampling_factor(sfizz_synth_t* synth); + /** - * @brief Set the internal oversampling rate. This is the sampling rate of - * the engine, not the output or expected rate of the calling - * function. For the latter use the `set_sample_rate()` functions. + * @brief Set the internal oversampling rate. * - * Increasing this value (up to x8 oversampling) improves the - * quality of the output at the expense of memory consumption and - * background loading speed. The main render path still uses the - * same linear interpolation algorithm and should not see its - * performance decrease, but the files are oversampled upon loading - * which increases the stress on the background loader and reduce - * the loading speed. You can tweak the size of the preloaded data - * to compensate for the memory increase, but the full loading will - * need to take place anyway. + * This is the sampling rate of the engine, not the output or expected rate of + * the calling function. For the latter use the sfizz_set_sample_rate() function. * - * This function takes a lock and disables the callback; prefer calling - * it out of the RT thread. It can also take a long time to return. - * If the new oversampling factor is the same as the current one, it will - * release the lock immediately and exit. + * Increasing this value (up to x8 oversampling) improves the quality of the + * output at the expense of memory consumption and background loading speed. + * The main render path still uses the same linear interpolation algorithm and + * should not see its performance decrease, but the files are oversampled upon + * loading which increases the stress on the background loader and reduce + * the loading speed. You can tweak the size of the preloaded data to compensate + * for the memory increase, but the full loading will need to take place anyway. + * + * This function takes a lock and disables the callback; prefer calling it out + * of the RT thread. It can also take a long time to return. + * If the new oversampling factor is the same as the current one, it will + * release the lock immediately and exit. + * @since 0.2.0 * * @param synth The synth. * @param[in] oversampling The oversampling factor. * - * @return @true if the oversampling factor was correct, @false otherwise. + * @return @true if the oversampling factor was correct, @false otherwise. */ SFIZZ_EXPORTED_API bool sfizz_set_oversampling_factor(sfizz_synth_t* synth, sfizz_oversampling_factor_t oversampling); /** - * @brief Get the default resampling quality. This is the quality setting - * which the engine uses when the instrument does not use the - * opcode `sample_quality`. The engine uses distinct default quality - * settings for live mode and freewheeling mode, which both can be - * accessed by the means of this function. + * @brief Get the default resampling quality. + * + * This is the quality setting which the engine uses when the instrument + * does not use the opcode `sample_quality`. The engine uses distinct + * default quality settings for live mode and freewheeling mode, + * which both can be accessed by the means of this function. * @since 0.4.0 * - * @param synth The synth. - * @param[in] mode The processing mode. + * @param synth The synth. + * @param[in] mode The processing mode. * - * @return The sample quality for the given mode, in the range 1 to 10. + * @return The sample quality for the given mode, in the range 1 to 10. */ SFIZZ_EXPORTED_API int sfizz_get_sample_quality(sfizz_synth_t* synth, sfizz_process_mode_t mode); /** - * @brief Set the default resampling quality. This is the quality setting - * which the engine uses when the instrument does not use the - * opcode `sample_quality`. The engine uses distinct default quality - * settings for live mode and freewheeling mode, which both can be - * accessed by the means of this function. + * @brief Set the default resampling quality. + * + * This is the quality setting which the engine uses when the instrument + * does not use the opcode `sample_quality`. The engine uses distinct + * default quality settings for live mode and freewheeling mode, + * which both can be accessed by the means of this function. * @since 0.4.0 * - * @param synth The synth. - * @param[in] mode The processing mode. - * @param[in] quality The desired sample quality, in the range 1 to 10. + * @param synth The synth. + * @param[in] mode The processing mode. + * @param[in] quality The desired sample quality, in the range 1 to 10. */ SFIZZ_EXPORTED_API void sfizz_set_sample_quality(sfizz_synth_t* synth, sfizz_process_mode_t mode, int quality); /** - * @brief Set the global instrument volume. + * @brief Set the global instrument volume. + * @since 0.2.0 * - * @param synth The synth. - * @param volume The new volume. + * @param synth The synth. + * @param volume The new volume. */ SFIZZ_EXPORTED_API void sfizz_set_volume(sfizz_synth_t* synth, float volume); /** - * @brief Return the global instrument volume. + * @brief Return the global instrument volume. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API float sfizz_get_volume(sfizz_synth_t* synth); /** - * @brief Set the number of voices used by the synth. - * This function takes a lock and disables the callback; prefer calling - * it out of the RT thread. It can also take a long time to return. - * If the new number of voices is the same as the current one, it will - * release the lock immediately and exit. + * @brief Set the number of voices used by the synth. * - * @param synth The synth. - * @param num_voices The number of voices. + * This function takes a lock and disables the callback; prefer calling + * it out of the RT thread. It can also take a long time to return. + * If the new number of voices is the same as the current one, it will + * release the lock immediately and exit. + * @since 0.2.0 + * + * @param synth The synth. + * @param num_voices The number of voices. */ SFIZZ_EXPORTED_API void sfizz_set_num_voices(sfizz_synth_t* synth, int num_voices); + /** - * @brief Return the number of voices. + * @brief Return the number of voices. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_voices(sfizz_synth_t* synth); /** - * @brief Return the number of allocated buffers from the synth. + * @brief Return the number of allocated buffers from the synth. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_buffers(sfizz_synth_t* synth); + /** - * @brief Get the number of bytes allocated from the synth. Note that this - * value can be less than the actual memory usage since it only - * counts the buffer objects managed by sfizz. + * @brief Get the number of bytes allocated from the synth. * - * @param synth The synth. + * Note that this value can be less than the actual memory usage since it only + * counts the buffer objects managed by sfizz. + * @since 0.2.0 + * + * @param synth The synth. */ SFIZZ_EXPORTED_API int sfizz_get_num_bytes(sfizz_synth_t* synth); /** - * @brief Enable freewheeling on the synth. + * @brief Enable freewheeling on the synth. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API void sfizz_enable_freewheeling(sfizz_synth_t* synth); + /** - * @brief Disable freewheeling on the synth. + * @brief Disable freewheeling on the synth. + * @since 0.2.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API void sfizz_disable_freewheeling(sfizz_synth_t* synth); + /** - * @brief Return a comma separated list of unknown opcodes. - * The caller has to free() the string returned. - * This function allocates memory, do not call on the audio thread. + * @brief Return a comma separated list of unknown opcodes. * - * @param synth The synth. + * The caller has to free() the string returned. This function allocates memory, + * do not call on the audio thread. + * @since 0.2.0 + * + * @param synth The synth. */ SFIZZ_EXPORTED_API char* sfizz_get_unknown_opcodes(sfizz_synth_t* synth); /** - * @brief Check if the SFZ should be reloaded. - * Depending on the platform this can create file descriptors. + * @brief Check if the SFZ should be reloaded. * - * @param synth The synth. + * Depending on the platform this can create file descriptors. + * @since 0.2.0 * - * @return @true if any included files (including the root file) have - * been modified since the sfz file was loaded, @false otherwise. + * @param synth The synth. + * + * @return @true if any included files (including the root file) + have been modified since the sfz file was loaded, @false otherwise. */ SFIZZ_EXPORTED_API bool sfizz_should_reload_file(sfizz_synth_t* synth); /** - * @brief Check if the scala file should be reloaded. - * Depending on the platform this can create file descriptors. + * @brief Check if the scala file should be reloaded. * - * @param synth The synth. + * Depending on the platform this can create file descriptors. + * @since 0.4.0 * - * @return @true if the scala file has been modified since loading. + * @param synth The synth. + * + * @return @true if the scala file has been modified since loading. */ SFIZZ_EXPORTED_API bool sfizz_should_reload_scala(sfizz_synth_t* synth); /** - * @brief Enable logging of timings to sidecar CSV files. This can produce - * many outputs so use with caution. + * @brief Enable logging of timings to sidecar CSV files. + * @since 0.3.0 * - * @param synth The synth. + * @note This can produce many outputs so use with caution. + * + * @param synth The synth. */ SFIZZ_EXPORTED_API void sfizz_enable_logging(sfizz_synth_t* synth); /** - * @brief Disable logging. + * @brief Disable logging. + * @since 0.3.0 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API void sfizz_disable_logging(sfizz_synth_t* synth); /** - * @brief Enable logging of timings to sidecar CSV files. This can produce - * many outputs so use with caution. + * @brief Enable logging of timings to sidecar CSV files. + * @since 0.3.2 * - * @param synth The synth. - * @param prefix The prefix. + * @note This can produce many outputs so use with caution. + * + * @param synth The synth. + * @param prefix The prefix. */ SFIZZ_EXPORTED_API void sfizz_set_logging_prefix(sfizz_synth_t* synth, const char* prefix); /** - * @brief Shuts down the current processing, clear buffers and reset the voices. + * @brief Shuts down the current processing, clear buffers and reset the voices. + * @since 0.3.2 * - * @param synth The synth. + * @param synth The synth. */ SFIZZ_EXPORTED_API void sfizz_all_sound_off(sfizz_synth_t* synth); /** - * @brief Add external definitions prior to loading; - * Note that these do not get reset by loading or resetting the synth. - * You need to call sfizz_clear_external_definitions() to erase them. + * @brief Add external definitions prior to loading. * @since 0.4.0 * - * @param synth - * @param id - * @param value + * @note These do not get reset by loading or resetting the synth. + * You need to call sfizz_clear_external_definitions() to erase them. + * + * @param synth The synth. + * @param id The definition variable name. + * @param value The definition value. */ SFIZZ_EXPORTED_API void sfizz_add_external_definitions(sfizz_synth_t* synth, const char* id, const char* value); @@ -578,15 +661,21 @@ SFIZZ_EXPORTED_API void sfizz_add_external_definitions(sfizz_synth_t* synth, con * @brief Clears external definitions for the next file loading. * @since 0.4.0 * - * @param synth + * @param synth The synth. */ SFIZZ_EXPORTED_API void sfizz_clear_external_definitions(sfizz_synth_t* synth); +/** + * @brief Index out of bound error for the requested CC/key label. + * @since 0.4.0 + */ #define SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX -1 /** - * @brief Get the number of key labels registered in the current sfz file + * @brief Get the number of key labels registered in the current sfz file. * @since 0.4.0 + * + * @param synth The synth. */ SFIZZ_EXPORTED_API unsigned int sfizz_get_num_key_labels(sfizz_synth_t* synth); @@ -594,6 +683,9 @@ SFIZZ_EXPORTED_API unsigned int sfizz_get_num_key_labels(sfizz_synth_t* synth); * @brief Get the key number for the label registered at index label_index. * @since 0.4.0 * + * @param synth The synth. + * @param label_index The label index. + * * @returns the number or SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX if the index is out of bounds. */ SFIZZ_EXPORTED_API int sfizz_get_key_label_number(sfizz_synth_t* synth, int label_index); @@ -602,7 +694,10 @@ SFIZZ_EXPORTED_API int sfizz_get_key_label_number(sfizz_synth_t* synth, int labe * @brief Get the key text for the label registered at index label_index. * @since 0.4.0 * - * @returns the label or NULL if the index is out of bounds. + * @param synth The synth. + * @param label_index The label index. + * + * @returns the label or @null if the index is out of bounds. */ SFIZZ_EXPORTED_API const char * sfizz_get_key_label_text(sfizz_synth_t* synth, int label_index); @@ -610,6 +705,7 @@ SFIZZ_EXPORTED_API const char * sfizz_get_key_label_text(sfizz_synth_t* synth, i * @brief Get the number of CC labels registered in the current sfz file * @since 0.4.0 * + * @param synth The synth. */ SFIZZ_EXPORTED_API unsigned int sfizz_get_num_cc_labels(sfizz_synth_t* synth); @@ -617,15 +713,21 @@ SFIZZ_EXPORTED_API unsigned int sfizz_get_num_cc_labels(sfizz_synth_t* synth); * @brief Get the CC number for the label registered at index label_index. * @since 0.4.0 * + * @param synth The synth. + * @param label_index The label index. + * * @returns the number or SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX if the index is out of bounds. */ - SFIZZ_EXPORTED_API int sfizz_get_cc_label_number(sfizz_synth_t* synth, int label_index); + /** * @brief Get the CC text for the label registered at index label_index. * @since 0.4.0 * - * @returns the label or NULL if the index is out of bounds. + * @param synth The synth. + * @param label_index The label index. + * + * @returns the label or @null if the index is out of bounds. */ SFIZZ_EXPORTED_API const char * sfizz_get_cc_label_text(sfizz_synth_t* synth, int label_index); diff --git a/src/sfizz.hpp b/src/sfizz.hpp index 957c7d41..ab39323a 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -6,7 +6,7 @@ /** @file - @brief sfizz public C++ API + @brief sfizz public C++ API. */ #pragma once @@ -29,22 +29,23 @@ namespace sfz { class Synth; /** - * @brief Main class + * @brief Main class. */ class SFIZZ_EXPORTED_API Sfizz { public: /** - * @brief Construct a new Sfizz object. The synth by default is set at 48 kHz - * and a block size of 1024. You should change these values if they are not - * suited to your application. + * @brief Construct a new Sfizz object. * + * The synth by default is set at 48 kHz and a block size of 1024. + * You should change these values if they are not suited to your application. */ Sfizz(); ~Sfizz(); /** - * @brief Processing mode + * @brief Processing mode. + * @since 0.4.0 */ enum ProcessMode { ProcessLive, @@ -57,6 +58,7 @@ public: * 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. + * @since 0.2.0 * * @param path The path to the file to load, as string. * @@ -86,7 +88,8 @@ public: * @brief Sets the tuning from a Scala file loaded from the file system. * @since 0.4.0 * - * @param path The path to the file in Scala format. + * @param path The path to the file in Scala format. + * * @return @true when tuning scale loaded OK, * @false if some error occurred. */ @@ -96,7 +99,8 @@ public: * @brief Sets the tuning from a Scala file loaded from memory. * @since 0.4.0 * - * @param text The contents of the file in Scala format. + * @param text The contents of the file in Scala format. + * * @return @true when tuning scale loaded OK, * @false if some error occurred. */ @@ -136,6 +140,7 @@ public: /** * @brief Configure stretch tuning using a predefined parametric Railsback curve. + * * A ratio 1/2 is supposed to match the average piano; 0 disables (the default). * @since 0.4.0 * @@ -145,57 +150,68 @@ public: /** * @brief Return the current number of regions loaded. + * @since 0.2.0 */ int getNumRegions() const noexcept; /** * @brief Return the current number of groups loaded. + * @since 0.2.0 */ int getNumGroups() const noexcept; /** * @brief Return the current number of masters loaded. + * @since 0.2.0 */ int getNumMasters() const noexcept; /** * @brief Return the current number of curves loaded. + * @since 0.2.0 */ int getNumCurves() const noexcept; /** * @brief Return a list of unsupported opcodes, if any. + * @since 0.2.0 */ const std::vector& getUnknownOpcodes() const noexcept; /** * @brief Return the number of preloaded samples in the synth. + * @since 0.2.0 */ 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 + * @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. + * @since 0.2.0 * * @param samplesPerBlock The number of samples per block. */ void setSamplesPerBlock(int samplesPerBlock) noexcept; /** - * @brief Set the sample rate. If you do not call it it is initialized - * to sfz::config::defaultSampleRate. + * @brief Set the sample rate. + * + * If you do not call it it is initialized to `sfz::config::defaultSampleRate`. + * @since 0.2.0 * * @param sampleRate The sample rate. */ void setSampleRate(float sampleRate) noexcept; /** - * @brief Get the default resampling quality. This is the quality setting - * which the engine uses when the instrument does not use the - * opcode `sample_quality`. The engine uses distinct default quality - * settings for live mode and freewheeling mode, which both can be - * accessed by the means of this function. + * @brief Get the default resampling quality. + * + * This is the quality setting which the engine uses when the instrument + * does not use the opcode `sample_quality`. The engine uses distinct + * default quality settings for live mode and freewheeling mode, + * which both can be accessed by the means of this function. * @since 0.4.0 * * @param[in] mode The processing mode. @@ -205,11 +221,12 @@ public: int getSampleQuality(ProcessMode mode); /** - * @brief Set the default resampling quality. This is the quality setting - * which the engine uses when the instrument does not use the - * opcode `sample_quality`. The engine uses distinct default quality - * settings for live mode and freewheeling mode, which both can be - * accessed by the means of this function. + * @brief Set the default resampling quality. + * + * This is the quality setting which the engine uses when the instrument + * does not use the opcode `sample_quality`. The engine uses distinct + * default quality settings for live mode and freewheeling mode, + * which both can be accessed by the means of this function. * @since 0.4.0 * * @param[in] mode The processing mode. @@ -219,19 +236,23 @@ public: /** * @brief Return the current value for the volume, in dB. + * @since 0.2.0 */ float getVolume() const noexcept; /** - * @brief Set the value for the volume. This value will be - * clamped within sfz::default::volumeRange. + * @brief Set the value for the volume. + * + * This value will be clamped within `sfz::default::volumeRange`. + * @since 0.2.0 * * @param volume The new volume. */ void setVolume(float volume) noexcept; /** - * @brief Send a note on event to the synth + * @brief Send a note on event to the synth. + * @since 0.2.0 * * @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(). @@ -241,7 +262,8 @@ public: void noteOn(int delay, int noteNumber, uint8_t velocity) noexcept; /** - * @brief Send a note off event to the synth + * @brief Send a note off event to the synth. + * @since 0.2.0 * * @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(). @@ -252,9 +274,10 @@ public: /** * @brief Send a CC event to the synth + * @since 0.2.0 * - * @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 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. * @param ccValue the cc value. */ @@ -264,8 +287,8 @@ public: * @brief Send a high precision CC event to the synth * @since 0.4.0 * - * @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 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. * @param normValue the normalized cc value, in domain 0 to 1. */ @@ -273,62 +296,69 @@ public: /** * @brief Send a pitch bend event to the synth + * @since 0.2.0 * * @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(). + * than the size of the block in the next call to renderBlock(). * @param pitch the pitch value centered between -8192 and 8192. */ void pitchWheel(int delay, int pitch) noexcept; /** * @brief Send a aftertouch event to the synth. (CURRENTLY UNIMPLEMENTED) + * @since 0.2.0 * - * @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 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 aftertouch the aftertouch value. */ void aftertouch(int delay, uint8_t aftertouch) noexcept; /** * @brief Send a tempo event to the synth. + * @since 0.2.0 * - * @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 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 secondsPerBeat the new period of the beat. */ void tempo(int delay, float secondsPerBeat) noexcept; /** - * @brief Send the time signature. + * @brief Send the time signature. + * @since 0.4.1 * - * @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. + * @param beatsPerBar The number of beats per bar, or time signature numerator. + * @param beatUnit 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. + * @since 0.4.1 * - * @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. + * @param bar The current bar. + * @param barBeat The fractional position of the current beat within the bar. */ void timePosition(int delay, int bar, float barBeat); /** - * @brief Send the playback state. + * @brief Send the playback state. + * @since 0.4.1 * - * @param delay The delay. - * @param playback_state The playback state, 1 if playing, 0 if stopped. + * @param delay The delay. + * @param playbackState 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 buffers must - * be float[numSamples][numOutputs * 2]. + * @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 buffers must be float[numSamples][numOutputs * 2]. + * @since 0.2.0 * * @param buffers the buffers to write the next block into. * @param numFrames the number of stereo frames in the block. @@ -338,20 +368,24 @@ public: /** * @brief Return the number of active voices. + * @since 0.2.0 */ int getNumActiveVoices() const noexcept; /** * @brief Return the total number of voices in the synth (the polyphony). + * @since 0.2.0 */ int getNumVoices() const noexcept; /** * @brief Change the number of voices (the polyphony). + * * This function takes a lock and disables the callback; prefer calling * it out of the RT thread. It can also take a long time to return. * If the new number of voices is the same as the current one, it will * release the lock immediately and exit. + * @since 0.2.0 * * @param numVoices The number of voices. */ @@ -359,6 +393,7 @@ public: /** * @brief Set the oversampling factor to a new value. + * * It will kill all the voices, and trigger a reloading of every file in * the FilePool under the new oversampling. * @@ -376,6 +411,7 @@ public: * it out of the RT thread. It can also take a long time to return. * If the new oversampling factor is the same as the current one, it will * release the lock immediately and exit. + * @since 0.2.0 * * @param factor The oversampling factor. * @@ -385,15 +421,18 @@ public: /** * @brief Return the current oversampling factor. + * @since 0.2.0 */ int getOversamplingFactor() const noexcept; /** * @brief Set the preloaded file size. + * * This function takes a lock and disables the callback; prefer calling * it out of the RT thread. It can also take a long time to return. * If the new preload size is the same as the current one, it will * release the lock immediately and exit. + * @since 0.2.0 * * @param preloadSize The preload size. */ @@ -401,30 +440,37 @@ public: /** * @brief Return the current preloaded file size. + * @since 0.2.0 */ uint32_t getPreloadSize() const noexcept; /** * @brief Return the number of allocated buffers. + * @since 0.2.0 */ int getAllocatedBuffers() const noexcept; /** * @brief Return the number of bytes allocated through the buffers. + * @since 0.2.0 */ int getAllocatedBytes() const noexcept; /** - * @brief Enable freewheeling on the synth. This will wait for background - * loaded files to finish loading before each render callback to ensure that - * there will be no dropouts. + * @brief Enable freewheeling on the synth. + * + * This will wait for background loaded files to finish loading + * before each render callback to ensure that there will be no dropouts. + * @since 0.2.0 */ void enableFreeWheeling() noexcept; /** - * @brief Disable freewheeling on the synth. You should disable freewheeling - * before live use of the plugin otherwise the audio thread will lock. + * @brief Disable freewheeling on the synth. * + * You should disable freewheeling before live use of the plugin + * otherwise the audio thread will lock. + * @since 0.2.0 */ void disableFreeWheeling() noexcept; @@ -432,6 +478,7 @@ public: * @brief Check if the SFZ should be reloaded. * * Depending on the platform this can create file descriptors. + * @since 0.2.0 * * @return @true if any included files (including the root file) have * been modified since the sfz file was loaded, @false otherwise. @@ -442,23 +489,27 @@ public: * @brief Check if the tuning (scala) file should be reloaded. * * Depending on the platform this can create file descriptors. + * @since 0.4.0 * - * @return true if a scala file has been loaded and has changed - * @return false + * @return @true if a scala file has been loaded and has changed, @false otherwise. */ bool shouldReloadScala(); /** - * @brief Enable logging of timings to sidecar CSV files. This can produce - * many outputs so use with caution. + * @brief Enable logging of timings to sidecar CSV files. + * @since 0.3.0 + * + * @note This can produce many outputs so use with caution. * * @param prefix the file prefix to use for logging. */ void enableLogging() noexcept; /** - * @brief Enable logging of timings to sidecar CSV files. This can produce - * many outputs so use with caution. + * @brief Enable logging of timings to sidecar CSV files. + * @since 0.3.2 + * + * @note This can produce many outputs so use with caution. * * @param prefix the file prefix to use for logging. */ @@ -466,52 +517,54 @@ public: /** * @brief Set the logging prefix. + * @since 0.3.2 * * @param prefix */ void setLoggingPrefix(const std::string& prefix) noexcept; /** - * @brief - * + * @brief Disable logging of timings to sidecar CSV files. + * @since 0.3.0 */ void disableLogging() noexcept; /** * @brief Shuts down the current processing, clear buffers and reset the voices. + * @since 0.3.2 */ void allSoundOff() noexcept; /** - * @brief Add external definitions prior to loading; - * Note that these do not get reset by loading or resetting the synth. - * You need to call clearExternalDefintions() to erase them. + * @brief Add external definitions prior to loading. * @since 0.4.0 * - * @param id - * @param value + * @note These do not get reset by loading or resetting the synth. + * You need to call clearExternalDefintions() to erase them. + * + * @param id The definition variable name. + * @param value The definition value. */ void addExternalDefinition(const std::string& id, const std::string& value); /** * @brief Clears external definitions for the next file loading. * @since 0.4.0 - * */ void clearExternalDefinitions(); /** - * @brief Get the key labels, if any + * @brief Get the key labels, if any. * @since 0.4.0 - * */ const std::vector>& getKeyLabels() const noexcept; + /** - * @brief Get the CC labels, if any + * @brief Get the CC labels, if any. * @since 0.4.0 - * */ const std::vector>& getCCLabels() const noexcept; + private: std::unique_ptr synth; }; From 40e48bcc5468ed69fd9650e6c8e3d0d94140c98a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 2 Sep 2020 12:19:26 +0200 Subject: [PATCH 182/445] User interface update --- editor/CMakeLists.txt | 60 ++ editor/cmake/Vstgui.cmake | 2 + editor/layout/main.fl | 344 +++++++ .../Fonts/fluentui-system-regular-20.ttf | Bin 0 -> 263124 bytes editor/resources/icon_white.png | Bin 0 -> 3412 bytes editor/resources/icon_white.svg | 253 +++++ editor/resources/icon_white@2x.png | Bin 0 -> 7336 bytes editor/resources/knob.knob | Bin 0 -> 22266 bytes editor/resources/knob48.png | Bin 0 -> 10955 bytes editor/resources/knob48@2x.png | Bin 0 -> 109207 bytes editor/resources/logo_text.png | Bin 0 -> 5414 bytes editor/resources/logo_text.svg | 252 +++++ editor/resources/logo_text@2x.png | Bin 0 -> 10935 bytes editor/src/editor/Editor.cpp | 879 +++++++++--------- editor/src/editor/GUIComponents.cpp | 420 ++++++++- editor/src/editor/GUIComponents.h | 134 ++- editor/src/editor/NativeHelpers.cpp | 50 + editor/src/editor/NativeHelpers.h | 9 + editor/src/editor/NativeHelpers.mm | 30 + editor/src/editor/layout/main.hpp | 167 ++++ editor/tools/layout-maker/LICENSE | 23 + editor/tools/layout-maker/README | 2 + editor/tools/layout-maker/sources/layout.h | 42 + editor/tools/layout-maker/sources/main.cpp | 136 +++ editor/tools/layout-maker/sources/reader.cpp | 359 +++++++ editor/tools/layout-maker/sources/reader.h | 13 + lv2/CMakeLists.txt | 7 +- scripts/innosetup.iss.in | 6 +- vst/CMakeLists.txt | 7 +- 29 files changed, 2720 insertions(+), 475 deletions(-) create mode 100644 editor/layout/main.fl create mode 100644 editor/resources/Fonts/fluentui-system-regular-20.ttf create mode 100644 editor/resources/icon_white.png create mode 100644 editor/resources/icon_white.svg create mode 100644 editor/resources/icon_white@2x.png create mode 100644 editor/resources/knob.knob create mode 100644 editor/resources/knob48.png create mode 100644 editor/resources/knob48@2x.png create mode 100644 editor/resources/logo_text.png create mode 100644 editor/resources/logo_text.svg create mode 100644 editor/resources/logo_text@2x.png create mode 100644 editor/src/editor/NativeHelpers.cpp create mode 100644 editor/src/editor/NativeHelpers.h create mode 100644 editor/src/editor/NativeHelpers.mm create mode 100644 editor/src/editor/layout/main.hpp create mode 100644 editor/tools/layout-maker/LICENSE create mode 100644 editor/tools/layout-maker/README create mode 100644 editor/tools/layout-maker/sources/layout.h create mode 100644 editor/tools/layout-maker/sources/main.cpp create mode 100644 editor/tools/layout-maker/sources/reader.cpp create mode 100644 editor/tools/layout-maker/sources/reader.h diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 65cdb6d9..bd9a38a8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3,8 +3,23 @@ include("cmake/Vstgui.cmake") set(EDITOR_RESOURCES logo.png + logo_text.png + logo_text@2x.png + icon_white.png + icon_white@2x.png + knob48.png + knob48@2x.png + Fonts/fluentui-system-regular-20.ttf PARENT_SCOPE) +function(copy_editor_resources SOURCE_DIR DESTINATION_DIR) + foreach(res ${EDITOR_RESOURCES}) + get_filename_component(_dir "${res}" DIRECTORY) + file(MAKE_DIRECTORY "${DESTINATION_DIR}/${_dir}") + file(COPY "${SOURCE_DIR}/${res}" DESTINATION "${DESTINATION_DIR}/${_dir}") + endforeach() +endfunction() + # editor add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/EditIds.h @@ -14,8 +29,53 @@ add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/EditorController.h src/editor/GUIComponents.h src/editor/GUIComponents.cpp + src/editor/NativeHelpers.h + src/editor/NativeHelpers.cpp + src/editor/layout/main.hpp src/editor/utility/vstgui_after.h src/editor/utility/vstgui_before.h) target_include_directories(sfizz_editor PUBLIC "src") target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui) target_link_libraries(sfizz_editor PUBLIC absl::strings) +if(APPLE) + find_library(APPLE_APPKIT_LIBRARY "AppKit") + find_library(APPLE_CORESERVICES_LIBRARY "CoreServices") + find_library(APPLE_FOUNDATION_LIBRARY "Foundation") + target_sources(sfizz_editor PRIVATE + src/editor/NativeHelpers.mm) + target_link_libraries(sfizz_editor PRIVATE + "${APPLE_APPKIT_LIBRARY}" + "${APPLE_CORESERVICES_LIBRARY}" + "${APPLE_FOUNDATION_LIBRARY}") + target_compile_options(sfizz_editor PRIVATE "-fobjc-arc") +endif() + +# dependencies +if(WIN32) + # +elseif(APPLE) + # +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(sfizz-gio "gio-2.0" REQUIRED) + target_include_directories(sfizz_editor PRIVATE ${sfizz-gio_INCLUDE_DIRS}) + target_link_libraries(sfizz_editor PRIVATE ${sfizz-gio_LIBRARIES}) +endif() +target_include_directories(sfizz_editor PRIVATE "../src/external") # ghc::filesystem + +# layout tool +if(NOT CMAKE_CROSSCOMPILING) + add_executable(layout-maker + "tools/layout-maker/sources/layout.h" + "tools/layout-maker/sources/reader.cpp" + "tools/layout-maker/sources/reader.h" + "tools/layout-maker/sources/main.cpp") + target_link_libraries(layout-maker PRIVATE absl::strings) + + add_custom_command( + OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/editor/layout/main.hpp" + COMMAND "${CMAKE_CURRENT_BINARY_DIR}/layout-maker" + "${CMAKE_CURRENT_SOURCE_DIR}/layout/main.fl" + > "${CMAKE_CURRENT_SOURCE_DIR}/src/editor/layout/main.hpp" + DEPENDS layout-maker "${CMAKE_CURRENT_SOURCE_DIR}/layout/main.fl") +endif() diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 2cac1f17..25f185bb 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -211,6 +211,8 @@ endif() if(CMAKE_SYSTEM_NAME STREQUAL "Windows") # higher C++ requirement on Windows set_property(TARGET sfizz-vstgui PROPERTY CXX_STANDARD 14) + # Windows 10 RS2 DDI for custom fonts + target_compile_definitions(sfizz-vstgui PRIVATE "NTDDI_VERSION=0x0A000003") endif() if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") diff --git a/editor/layout/main.fl b/editor/layout/main.fl new file mode 100644 index 00000000..1d27e987 --- /dev/null +++ b/editor/layout/main.fl @@ -0,0 +1,344 @@ +# data file for the Fltk User Interface Designer (fluid) +version 1.0305 +header_name {.h} +code_name {.cxx} +widget_class mainView {open + xywh {410 523 800 475} type Double + class LogicalGroup visible +} { + Fl_Group {} { + comment {theme=darkTheme} open + xywh {0 0 800 110} + class LogicalGroup + } { + Fl_Group {} {open + xywh {5 4 100 101} box ROUNDED_BOX align 0 + class RoundedGroup + } { + Fl_Box {} { + comment {tag=kTagFirstChangePanel+kPanelGeneral} + image {../resources/icon_white.png} xywh {7 6 96 96} + class SfizzMainButton + } + } + Fl_Group {} {open + xywh {110 5 380 100} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Box {} { + label {File:} + xywh {125 15 40 25} labelsize 16 + class Label + } + Fl_Box {} { + label {KS:} + xywh {125 45 40 25} labelsize 16 + class Label + } + Fl_Box {} { + label {Separator 1} + xywh {120 40 355 5} box BORDER_BOX labeltype NO_LABEL + class HLine + } + Fl_Box {} { + label {Separator 2} + xywh {120 70 355 5} box BORDER_BOX labeltype NO_LABEL + class HLine + } + Fl_Box sfzFileLabel_ { + label {DefaultInstrument.sfz} + xywh {190 15 230 25} labelsize 20 + class Label + } + Fl_Box {} { + label {Key switch} + xywh {190 45 230 25} labelsize 20 + class Label + } + Fl_Box {} { + label {Voices:} + xywh {120 75 60 25} labelsize 12 align 24 + class Label + } + Fl_Button {} { + comment {tag=kTagLoadSfzFile} + xywh {425 15 25 25} labelsize 24 + class LoadFileButton + } + Fl_Button {} { + comment {tag=kTagEditSfzFile} + xywh {450 15 25 25} labelsize 24 + class EditFileButton + } + Fl_Box infoVoicesLabel_ { + xywh {185 75 50 25} labelsize 12 align 16 + class Label + } + Fl_Box {} { + label {Max:} + xywh {240 75 60 25} labelsize 12 align 24 + class Label + } + Fl_Box numVoicesLabel_ { + xywh {305 75 50 25} labelsize 12 align 16 + class Label + } + Fl_Box {} { + label {Memory:} + xywh {360 75 60 25} labelsize 12 align 24 + class Label + } + Fl_Box memoryLabel_ { + xywh {425 75 50 25} labelsize 12 align 16 + class Label + } + } + Fl_Group {} {open + xywh {495 5 100 100} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Light_Button {} { + label SETUP + comment {tag=kTagFirstChangePanel+kPanelSettings} + xywh {510 42 70 25} + class LightButton + } + Fl_Light_Button {} { + label CC + comment {tag=kTagFirstChangePanel+kPanelControls} + xywh {510 15 70 25} + class LightButton + } + Fl_Light_Button {} { + label INFO + comment {tag=kTagFirstChangePanel+kPanelInfo} + xywh {510 69 70 25} + class LightButton + } + } + Fl_Group {} {open + xywh {600 5 195 100} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Dial {} { + xywh {615 20 48 48} value 0.5 hide + class Knob48 + } + Fl_Box {} { + label Center + xywh {610 70 60 25} labelsize 12 hide + class ValueLabel + } + Fl_Dial volumeSlider_ { + comment {tag=kTagSetVolume} + xywh {680 20 48 48} value 0.5 + class Knob48 + } + Fl_Box volumeLabel_ { + label {0.0 dB} + xywh {675 70 60 25} labelsize 12 + class ValueLabel + } + Fl_Box {} { + xywh {745 20 35 70} box BORDER_BOX + class VMeter + } + } + } + Fl_Group {subPanels_[kPanelGeneral]} { + xywh {5 110 790 285} hide + class LogicalGroup + } { + Fl_Group {} {open + xywh {5 110 120 285} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Box {} { + label {Curves:} + xywh {15 120 60 25} labelsize 12 align 20 + class Label + } + Fl_Box {} { + label {Masters:} + xywh {15 145 60 25} labelsize 12 align 20 + class Label + } + Fl_Box {} { + label {Groups:} + xywh {15 170 60 25} labelsize 12 align 20 + class Label + } + Fl_Box {} { + label {Regions:} + xywh {15 195 60 25} labelsize 12 align 20 + class Label + } + Fl_Box {} { + label {Samples:} + xywh {15 220 60 25} labelsize 12 align 20 + class Label + } + Fl_Box infoCurvesLabel_ { + label 0 + xywh {75 120 40 25} labelsize 12 align 16 + class Label + } + Fl_Box infoMastersLabel_ { + label 0 + xywh {75 145 40 25} labelsize 12 align 16 + class Label + } + Fl_Box infoGroupsLabel_ { + label 0 + xywh {75 170 40 25} labelsize 12 align 16 + class Label + } + Fl_Box infoRegionsLabel_ { + label 0 + xywh {75 195 40 25} labelsize 12 align 16 + class Label + } + Fl_Box infoSamplesLabel_ { + label 0 + xywh {75 220 40 25} labelsize 12 align 16 + class Label + } + } + Fl_Group {} {open + xywh {130 110 665 280} + class LogicalGroup + } { + Fl_Box {} { + image {../resources/logo_text.png} xywh {260 125 400 250} + class SfizzLargePicture + } + } + } + Fl_Group {subPanels_[kPanelControls]} { + xywh {5 110 790 285} hide + class LogicalGroup + } { + Fl_Group {} {open + xywh {5 110 790 285} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Box {} { + label {Controls not available} + xywh {5 110 790 285} labelsize 40 + class Label + } + } + } + Fl_Group {subPanels_[kPanelSettings]} {open + xywh {5 110 790 285} + class LogicalGroup + } { + Fl_Group {} { + label Engine open selected + xywh {260 125 280 110} box ROUNDED_BOX labelsize 12 align 17 + class TitleGroup + } { + Fl_Spinner numVoicesSlider_ { + comment {tag=kTagSetNumVoices} + xywh {285 185 60 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Box {} { + label Polyphony + xywh {275 145 80 25} labelsize 12 + class ValueLabel + } + Fl_Spinner oversamplingSlider_ { + comment {tag=kTagSetOversampling} + xywh {370 185 60 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Box {} { + label Oversampling + xywh {360 145 80 25} labelsize 12 + class ValueLabel + } + Fl_Box {} { + label {Preload size} + xywh {445 145 80 25} labelsize 12 + class ValueLabel + } + Fl_Spinner preloadSizeSlider_ { + comment {tag=kTagSetPreloadSize} + xywh {455 185 60 25} labelsize 12 textsize 12 + class ValueMenu + } + } + Fl_Group {} { + label Tuning open + xywh {205 260 390 120} box ROUNDED_BOX labelsize 12 align 17 + class TitleGroup + } { + Fl_Box {} { + label {Root key} + xywh {330 280 80 25} labelsize 12 + class ValueLabel + } + Fl_Spinner tuningFrequencySlider_ { + comment {tag=kTagSetTuningFrequency} + xywh {425 320 60 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Box {} { + label Frequency + xywh {415 280 80 25} labelsize 12 + class ValueLabel + } + Fl_Dial stretchedTuningSlider_ { + comment {tag=kTagSetStretchedTuning} + xywh {515 305 48 48} value 0.5 + class Knob48 + } + Fl_Box {} { + label Stretch + xywh {500 280 80 25} labelsize 12 + class ValueLabel + } + Fl_Box {} { + label {Scala file} + xywh {225 280 100 25} labelsize 12 + class ValueLabel + } + Fl_Button scalaFileButton_ { + label DefaultScale + comment {tag=kTagLoadScalaFile} + xywh {225 320 100 25} labelsize 12 + class ValueButton + } + Fl_Spinner scalaRootKeySlider_ { + comment {tag=kTagSetScalaRootKey} + xywh {340 320 35 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Spinner scalaRootOctaveSlider_ { + comment {tag=kTagSetScalaRootKey} + xywh {375 320 30 25} labelsize 12 textsize 12 + class ValueMenu + } + } + } + Fl_Group {subPanels_[kPanelInfo]} { + xywh {5 110 790 285} hide + class LogicalGroup + } { + Fl_Group {} {open + xywh {5 110 790 285} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Box {} { + label {Informative text goes here} + xywh {5 110 790 285} labelsize 40 + class Label + } + } + } + Fl_Box {} { + xywh {5 400 790 70} + class Piano + } +} diff --git a/editor/resources/Fonts/fluentui-system-regular-20.ttf b/editor/resources/Fonts/fluentui-system-regular-20.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4e838582501533c67dc8a4f23293889f9795f258 GIT binary patch literal 263124 zcmdpfdw`Tx`Tujy`_B8mbJ@A<%Dms&7S8CZZD{=LtlNl0H{R!8cJ+eA(f7(< zCUWj4(sy2V!;%up7w;2!*WDo5DTMK({x;*r~p}9oGuaNe&NsB(V zpeL_3{ye@NKxkQn2;(lT0_BJBUcTt+C4KuJf29uZsNX8-z4o#N<|SY4L-`ws{7qLc z=v%DM6|?YtIMSC~v*7BFy?3kk1Zh7-emPZwB9i{2(359^8ggT7dT8TRuV` zLI(%xqfY8U=%gEI23d3iwW+rr^{t0iAw>^DAKp9h)}wwe)1T=x)K7fBkuFnl{nUzk zCv8^OPL$S%6jP9=gI>eTU59T|(!WksS|@7jL^+(R1L^xwRwqj9pt-aL*H!d5eq7^5 zItSmmrF&=^?q~j;i#(61Z!fD_e~R?0{^hS9^>G~>`lu53kqDfF^r zgX8VDI<##SU5wUWOm+0UYDowE5AC2MB3Jc|`@;3wKd#OGad}k4mIa|=RR=C9=ZU5pE>nKn5`}~WBk4cvE0Us=@IG_H;Nlo4$j$6 zrE28+aph2n)cos5;MzHsTVwaID}CoU{=z@b!>!ni-wfRERyjATI_)<9t7{VJ8K&(1 z+dZ2>y()&+#SFSom0XJFPMV7AT(o!%>N^LJ^04}5=gM5|vHuhO32;4qpand8cHkcK zYT_1U=ErWGJ*NHY+k@)cW?Xp%O;H?C-gBuBy3@ z#$q;>U@h@i8@g@C%CtOF!gN4EA6>0Rmg_7<;Jf{^YqsH^OL`FHaycE;K*RC$Ao|R2 zoTq`dr=R$JjhIK1RA2aQ3{rAyc@|fp4*tD~V2|Gt#4|?WQa0nc6Qy2)-!xSomv2M5 zji(ya{M)tKqnK$e&*90o+oMxScPjW`7-MX?=P_M{FPM zQEP|u*!gb6nq}C(5x;YRr|iD*sB@lq>dK|hz({w}rF0W!gYW&o7`@2%3H5y7Hpqi`Mqj}5o_i}aqj7<0(IBGq3jg#@B!H`r$V=zVY~Pi+&sZZP}r0@NjgHUVr@cpT7Rq8;`%ykJ!@R zmUCxqqwh!Gi~bYkG3R#rKdwiTP7X521TW$w7rDs;KGRD+)Eb~{3LCkZ^eOr@t)=^D9o_6wT-?^&(c?bm%mD1qvybte4V}lE@dZu zlU@LqvWvb&FVai2o4!ro0cW>|zDwVu@6#*v19}y_=U(~|{g{42`{<|O=YB@}>F4wd z`X$!cujtqG8=iW4gMLeg=y&vc`U4#XCU}efNN>|Spoo9QO8pD{mHtLY>F@L|{ezCt zKj}SspN`WBO3?>kgN2Yn6S{B+Lzu!6PT>-6;SpKFD}2H)0wP-kMM#82j);g{ktgy+ zfhZJ3qF6*li6|9iA|}d3g@}uUs1#LVh^Q7dVyGA7Y(9OG>K->B9dab7$HWA zRxwJ97Gp%4Xcy;*4l!1Y6XV4MF;Pqslf@JL$HjHxda*<- z6*qtj>JvAKo5d|+nYdNlCT%{}&LGh5-ARZQvh)2an@fd^%10lVhgv0}f!T&NJ2gm*Y_N-(|)hWn&fM*h; zFc3Wm+kw^~`)LOueH|k*fPDb zg_aS8mk{MlB8s5=T)gKkC(7?5DnOoswFpOnD!UQ(5*4*0Y=s<)I*O667sy%Im#+OLR8UVrf>@qH-1 z4XZ)eL{y9KwOfel#v$N){YIh&JU8qoYTQNCw2i2FB?9Vc=|MnTvK?Uu(Qu?2zME*o zGz5Ha)e-uLMj_p3lsN`z+j0@M6Sbp1=gdObOVqJ}XlyUSF`{vsh{ogF1e7_^kAS{S zLf*-Un}WEhh?`b|aFl3zA<+yE0`hb&LpV%y9?F?H4&fD|E`(Vl_aFfU6J4Ss>>`@~HqoU>e<|v`40(ETi9WWP=^R@2e~X#NGKe(duzT zccE=}?XhVXbF(Pz-U&uk&;-$nGL$rqSuh_HPrbtl)E44e~$NGc%a$ohH5F7=vRpU^()|F(8f2=pWlu`I6!m=ZT)>6 z(I4=9ct6pbYl!~1gXo>b2*-&2jOV}L`(NHB`fCjW+V(e;aTMwPzK!Tzy#Hf3(J?;) z(*AQT(fer6`+JFwBb-=5^uaVB@DdQgZW6#*LLUXBj(1}R32PY%XDNq85L@L34M5f+p1Bd`A;iEN|`jzic*BAknWv=KkTb`rV$B=Yb*AI}9#NED*3 zBIGSb8Bsi!Ag<&XiP8fk$~KUQq0EX9i8#tjpg!OtQMHA{kV7P@kCPZ$NCNmr)J`H% zhqlz0lK{>Ujk8EJq1wCxNLIOAeA)x|PHY=_q%G z(asn8N$grr;>B?!UMeT??OYPySxMsMCK7u%ApLjo?R$$!yt0hMt9X9(D2X2;&0h53 zM>|OT80Gz>o5ViU^HY@f+FlYrBLw8xUxM&9iJu=M@k>7fz8yflznV|t*C_wj2TA;9 zH;IGmNxY8tH<0GH=<^|@`Q2O+zpo_m2c$i0A?zgaCfe{8%6tpuy^Z@jvq=02ZTa(d z5=ZJt{Dlxu-d{J8_#4t6olD~HJqX81{G*V>v0f6u0pg!WNxZ*|#PJZqK@ukrmqHyW z#DB07Jls)|;vh+RjHJGaq+>rxqa9%_NpmqtYZAg?lFlt8U4@&#!)1j z4wG!&N3sQZlE+Do=taPNsONm?;)2W{SC`V-iR{$JP1gC(=-Ic-Moq9EvRQ1^4*Gbw;d+A zyar)A$rUJj#XgdEtRcCwpX4gsfj{IYcamI<=exQ|-aU%sJ%uFi^^^P*>iYB|l53Zd zTvti*{^bYl#XgySTCiSmC{jjtS6bOL3oAa z2ffhZgb;R;CKizfO49Vu4NQyNu(K$e8UfuHUjY@`$=nEPTDBkN248MmXOxgPFj08Y3B?_I7(W_ zCep^DzOi^7A0ll6+A%Q~VGn7OwvaX%PTHlY5BNpv z;joXi%Quj=XclQ#AY6$$uEP7(7HQYiAgm|t+GV6I-a*=R{iI#Lk+daf&(h%t$P4~M zyWtpVHy$VLCgi!foU~gukhW|mX}971Hsrq@`IdK+wjzXZn6yvqCGCzwq^(4{Rf|Zw z6W>3H=hb+=YdL9mk0b4#QKYTePTIYbNc+@U(muVIw6(`byYCg!?q5yX`rV{G&`H{Z zxIct+8`hBa$TZR(T}j$Tq<;+e&-9Q6*+l#7LDD{Vh_p>8>kCM?xej4JX{zBSbK*%VI zj~1~o-;7gvf7j1edtIhR0km z!I5xWdzDe)s92yPmINK4UL&r{(L1xfZln-aFqo*n-L;IfiNY1TN;us?n-s3;z=FSQ)+;VXsUKgYJzZ8;YUGYiw>p z?qXRa(gVb!)YPJ*>1D$N`qYr4hjJQPn&M^eMPh{&rs)bevqix28WUC9dtAP_D-7ErAKKP7G=*z#ZdqBbSk70O9Gpz0ugoMv zmT#2hrc^pkjaEsF)hLYBAxc6+4-qy#MkHGSN>Pz)$%z2gydst`)#?C|4S} z{nOEHE|7-PaC+nd>F9Q(ju{^Mz>(t@DAGY60B`6DhF+5^sGMqG;kXE6!wc6Zu`sYI zO=B2z<4ue{l8HFhC68CK#l%aj%!mqJ-?d2T#lj8cU=`;eg70cUb0UEmx97X5MUI;| z(jUtI;xcblEbQ>ylv`C;R_%&37Uab$L;foRUSIoL(>e{ym*spb9Qmi;iV9tqdA`1! znC{VvoXd6NDJ(z7(8jt{r|f2MJ+TE>xm--oYqVPWvn^jmPU{det0K}D$#;h90wTwZKZ5o>qg0XP1h%`2|B(*XOPZ83UHr8Sk zhY7KE-rXi_6GE6?r^B&E8__vJTjOvzoNfb_j|%;s$NFS6Y(!#m)Qsy4A>-oLGA^|x za__PWozArvXqxVD`rVo^6D=(XQ%EfzudZ#4#frmC=a!{l}&_t@YO2xwxEr z70)qjfq?xVfi~2_;?>3Y850P^6A_pjv^@qa94i|p(>)PwS~Dy#cHvfZ4!O;9rtRNt`b6Hv3H?bu3q8qq4E^0D;{1ke2-pr9j z6E4dPi?dAprLOu?!|yRpY~ky5W?prkFRv_!zB(da?&>iyCci*+u+r}E-Kw9-!m%gE z$mhnQXZvB7mJ7O61bbjTYZvsWa{Qq^Zn zC#N=Tvf7d?O%t?%h>9Ga1WApz$(9^Pq!{agSH&32_{Z+khiSQi$6Ko^N6N=7%b#Tq zTlYv$Q8-v68@iSb6>kmImi%%oh>KIh)#Ey`2sguJ@LQHs{v^w%bvQ$YeLJzR!>MJ3 zoazbz<73X%S6I$KUR7%{Vd!Rw|BBU%)}8o8s5n0;{<&n<{Rfiei(Rt4#w?ztOzC@4Oro6VpSGz12iP@c3Kob zD=`>A07<-5P6M&i7mHV(wpw(L)#-5?qpEqKr=Qk;D8}RQWWdJ{09kVe*c-gA5)+%s zw391K?Dl)Yt_!f@aqSy;!h8QoaPo$A@nJwbWz7}To>P9xs^c+iqp9*o^2e}P7r+|b z0&6l>UQSd4eiaTQUaQ4hM2;6oHl7F$15OZ8ts;><2A~;HOyMxlDFBm#ROaUraRBKU z@%?EVEA;vfx9-tXw`Ao5m(|Bci%t#fPH2c{PT`8SQH{A;lP<0t19-E(YPka*{laOc z)iBy2JOQ_GUSMiYznoAue2R6=Ij$m0K4o~l`o-5w?@!&}Eyw}I2#U+v>I(~Q8DlM- z{n=6Ca<8sm`va%PZ7saBR-agUQM1#sUog96 z%F0ng9>040u&I$;6Zj4vgzoTqL_OxL{ld*84d4@QY!`R?Irt562 z5<99|TdPu&s#<&UVzE52gRiN%kw9l4qAq=%oqcEhCVMlbq2!||Xi}!ozI1UYsYt z7hsbF&T$Ft*2~%^f@TbqE+7p-9AoEV>~6LG=dCyS2(lEjOxLxgaj^i>c=$rBW7$ z9U|y)>TlY`yy^0ahUZqHBqz|U3vWFvuW()Jjo0r5L7a#Mn67fX%qp&yBM~Z#9V4rX9Pgd>`W{-% zB#`eOS8B&dG#H&3DZV$7AHDKd$e4$a+Krrfc0v3uMy1~yko?4P(`9l#sdI<%klKw0 zaZPF+p2aA)7k$pi=uGD(!gq()hYiZ>% zH#JN&*Eh8|Al5fAfmEnUv0G7isf6n>kb7!NkCfJiLXp6cKqQ2J=?4`(rIR_IVT*Y^ zFCNAWRVU<3@v}3^N;}SYGQ`gNa z&Jf2)+%X`}G47~>SJcG{3I=30OgI+8se&6*F6N#zW|JZ|h$!JdUMrKu!Y3Iv82H;n zBTEr1CPZa&jF6p!M7HH81-7!(-hd!&9NA7yfBG`4D8NDyI4fPVw1@B45&b!+ z26#yP^HW9krJMPcetw5BF13PZ6_%~G{0)p7+Hm^is9^}D9Ylbb|A)8vMl zXzF*-nowC-6hv#(y8YjR;iUPm0rlU2ckxk z_#ybYpIo|Z+tbUcOke0uulb-EHpiOYuvx##lz*6ow=T;UHmk(Ch2yRtd&hiNmhii; zf9#oCZ`md)QCRAC`1+uUJYla{ztRE?V!hChm3%e~K8daHJ%m{*3BY2?lU7tfVnL&$ zQmrQ}Gp4&{OoRF`3N0TK>4qf8P^}0zCLp3IJzA|u#vzwPWe(($TCv?%;FsP!pRpYR zX19M>wtlT&^XYS)c!eV3$bl?P-*aToo`B|mWzx#+0Syt~^?0>KZcDquouyynamhZ; zFLrQA$l|)luX**W{j~u{H(t41q3`iJ#DN2T9R)0&g!>+jC`I0@-6+fBlvkikg=@Iq zw(h77ewbt7!wM&bv~U45qVz8|`os!r$N|@f@knE5B;z57X~+O}59%Yc8H14%X=n^) z&~~Hj?qn>=I@E!O)Ldp4w&%t6@O{r7PH^Oa%MxM_G_IECfZTFQdd_%aPljv31F{PJ zfSvZh9?LDnD_jN~MBqv`pbPbZ=O4xr0Nj$UftTl}Kv_<5OUy7ZRzw#-Q}7m1_yn?m z(u^_VPUFp^~prt6n?*sD!@?a;+=&@KA8UY{2vOa5T)EdcX z8w2T4RBW8mIaUCMAQIR@J=mBe=o+#pg(O3iT%Q-q$(}hfb$H~=>>NPA(Nqao&fKcj z)IO+4{knbGbK)4!zf#%Dy|KK`VAM?gXynY9BSozl4R!+3Qu~-?10nL{s zXPCw24D0nfL4$m*^p$n9cY;XC+Tv_E!{gLuNS{xWZkJYdn`XHkpv$*a$<$_Kue!|z zVG*x*_Hkm*3^OctnQqhN!zH!C?S%p!Nwo8#e6U0Ck3R`n^(0ZQHSJ+m>BFnS(CM+xm(%2Wq}k9L%yu~~ zuLgn99nM*{4JoG$=MbE9ClfwUdn-K25bZC+v1vPR(mMUD-j^BedmZ=q&4?J1NGvpcV#p z#cJEY&IB6EKi{z8KAIg!sb+?>e&voaFK^@rJ zD3{CS(Em`jqFrD|*glt*6>Q+NWr$=8G(1?oKo8sJ4R2@|K5yTZ-1IdD{-fzv?weLq zSy?k}yCr-4p0?qi8{T#$$KxXc5?5#x)fCcgn)LWjZ*Tw~~3^C*=4kt&M z=M-ZUE9+m{#b!opd&j(a9s7E@xKBYnq2Fr++(z%dF7dI=OEGivR?h3?I`4(b8sFRj z0}55H?`4{f`T7xX7zJ%+i!F7)PXHJ&CD6hswJ4VAXz>=8(65=6soB#NEt-%XUkP(0 zCJO=&J|Wd@$1#jU28P(K>(8jG8+9>kBv{@shAI0Y_<%Gs%kaT9%a$}Kn5V;t^bSCR z{vjwqf3QnN>HfJ6%`k_ESUjQGxjD_gK1VXs@I3 zLR_2rqfn=*Dn zHqU~Zm|>qIW~Mn}1`?o6MQoCdA_Tm0Y@1lWcip%O9MJ0vWifDip4RP59*?De`e zUqHVK&vGX(>tnYe>AJm8;yNDEbxlWlRyH8(RY-@_y%*vctvP8!EW$pW(HL)=?!|#+ zRQ+uN!>q_%L;(8_mPO6M_Ct_wnLzhfq5rE)6J+Ni@G;D1T;vO+ww|;jwq<3^h%9r8 zhe7*R`LmEGY~ps|Z&(Io_U1Pi`m;pIV0p54kdVr-D6!5uvA*|Whtd#ObrnNlvvA4A zU<1@985)g&Wh%{G0{qBc<{wg%_pXoKb^*M)o~m4j z1c=|ROY2!Ic)Z;we#Q)h0L8o&_V-62ncDVOwzp-7A6SV7JWCi^Sw4{82zwz-j##GX zvbNdh(KcIQumy6Ncp17AkV)6inVW=18^CaNVU0ds-PO>N9PV=1)CG;Xs6{zSZon-lWMxIM9WeDpU zhuufktfnwH-iKsWl@=(rM@I48@EGx`kvYj@15xoB?xY_BK4vk!X1*Q_j}IC|wkVdC z0q!B+aihUadA#yTHV&}iKs@0HFeX@iVn2j-T~@BfiyY6mcgZdXNsFRUmHzGZ93y(hdZKOZKIDA)X6pt?QawM*r_^XPCeNhLJ z)<7p3KqvkL`839UGLT44@U?;jp%hFvq|?V1OPMu zWN=Ky%pROZ9yX8x=^DPuxZdm;X$$YdYVw$;-SstV4NbP22 z=3Ep1SwVRVck~wVoq@;tfzP*QvJ4a==ba&0>L(`)Pp6zakYk|QsUNPi-UOa{jrKsU zmpyDm0HZK2z1!!+y+I^OIc_GnkV%tGfIH3?Ka45-VDza@K75VRKz07!#Hz0 zr%}*_&wh-x#6IRdvL#bjka1!dhyzN;<`xH!AbTA&9>c&l57MA6fqmdJk@|X@yLa=L2jos$#DJX(sNtq+XV1!- z)FDT}BMnj5viVkb%O@1>p9lQ=B0Rn>;4y`3DBI>)5f(e0ZETfr&VyJAkcqdjMk=jy zsg|$-L8wDMAOU6BguV%WPIA2odwI>Zoz5eM(SdQsF!$irV^7674&zA2ymH<1om|9& z`;tA1U%v@X^P$G~VM#y}CLdY%AWuojLn zVzo>(HZ(G+Zkd7}LQHkx))xqP{^f8Oj@0*uj=+#`zehJ-DP_AF!m5hGP_ZBJpFM2= zcUQih9$NVV`hH@ImWA7my7=%I!R|D(frm+r#mEJDS85x1Kx?}S3w@RK`Su8Ye(XW2|j-PYQ<$dfl116CC6`mnw=rWUO(P> ze6`rI+GbXsz!auAk}ut)yhz61FhRrK2F#INUm3hKl@$1)N|;ejH^KqZ!h+4ohKcp4 zd-ckbm99LcNvS>Qc5U2vvZZn*b4G*PF}RW2>~?P4I4BZpcM3g-aaqA@H<&bAp*dnk zT8TC+(hNou+`9FvS08s?vCiRgU~!(ey7TyIMi&9c6SzGgy+90N(dyOuLsuA12Y_J4 zo#%d1dL*5t1Syss0=`8vwF=Q9fM%(kM98Q+!)=n9%K{)%gDYAI0|3{!Bb z2+A`9vaT~$ zlhvIw^+IkdBK~no+t7q1U8~C`pX|Yq2QJ{oB4W~q+IEBTJ%0vT3R}mb^gO&l0EAP` zDdZmm+BsOVs}b* zoG_)?XTj%lV(JRfDhtq*GDB8~(5pS=6ua%MImR&}`>;io2Zns`5hbJnB zX9YZMzp1nwAgg84X{6rqV1t1ztNaMEa{=Q8XjKN(ifNf8qx4IbFwM?z&8%@5(AXVu zfXB3zq!>u0HoZqT#i;c2-40VfnF5TuL}KvESEd*?EU{WCV()QCQ1-se^FSi8+QR%x z=z9B*^-JJCYw>O~n;#OR4CW*s&5YS z?JAAj=;4$Ih?#>f%`uJ-JfQ&l8`{)E`EsXb09V1fn$24>OoNrNib@S|hzx{n5XBf4 zpxNgp+FFSL)hb9`BF6l|$rk?&4s*V&Y11ZO?h&%h0bTHHhhsMM#g5AB&g<^>1ANA!LE;M={&@=kKnd1k<)p(Rb^`Bk=w?qsSYpqBOitf5vBqXNgKhD)Dg&;Sj^d$( zby?Wh20nAtKvbxrz(SO5^z(ayZKv(5SmM{A((&ts(TZBe*ri2z1`9%K>^EOQwn7F- zu<3JqJP|qen_-8AkJTO;gM4=(2Qepup1Cxw~oXXw7b_5qK zU9N(VtKiD@;byabc2mA9RN(4zt!p-Wr>CFsYNwFV6I-0YLYM5uRo>UdNedj0T-DOh zl%L;pK~l?e6$YKNTm_%T`;6-ynO78$NtwD+c5|W=TX1FihVvgm-@4UqsX_WbtdPtK z2ltf(=2C`~9QgybCB2JPFbA@w$2h=Ghj;NX9nqm~F?BHM9&6XT1}DJkIDm)4$ik;g z^@IO@8vI}k7$t1`qY7n405#_w9H-6xo(INk?sd1NzK#jP*+df?oae7>xJS_ko7a{* zKl6ze_pO^-Qilu)NDzfJD|?*PH=#P4V@I8~P2oqZ_rR8{8(KlwRwo=?lQUvwv3%ho`Lsd5oky?bJ&2jgnGlYjh4w~ zDB;)*Ox&osJi_OM!9s$D1pFyD#yVX-mh5@c2NWwi5rIT0hnDR_8gcvu-RbeV--k8^ zY9(b(7{s&Wqvl~~NE+zVa=!u;<$xJ$NwYF+mZCHtAm(jjY4&M|_+IyZc7E`>UmxH| zQV%H}M*Q|^mV~!H!UFw%Tc_;uSp%$2dfOPrU@pdh{aGr}j>bqM=#(B#>rAk>0{SOb zWrIz^P_V)&DZX?Z{nX**h97i~mZ^)B0}55}KoA(v@OV z6YoRCY@1Pl>v1lf?9JFBL}`r0GWe@9YoxIl%X6W02CX88xj1eQL|oaOnxv%F9k?E{ zJnNLO`mEuCIf6x6&FfyL$t_CUg~)qi3kc%R?z0)wXDz9-Sc|uHsY>0JqdVv&;HHZe zZo<(YO>E9$qysG%Z+8R(&n9`Sa1*T27zyrCiN8r@%d;&A;%KpFhiUHc6z?hUX!g_A zn{@{S;P+Vhe)G-fIBZ_x=+uD6{fwDoKI8TTrcU*Fozi~G@=xtqIz){rSP#dLrAvok zKz%NlX`ojb&=uJ>r^)cUSj4lYEMd3~HZs``uB`%m&?y_%OxsA2F?pQga04ljhkd~9 z7iF+3ta8hOyd*B4Ra7IZhPK0*4}J*kL#tq4nYARfJ2wX9(sGwCHYKmDEN?%1z3kub zbnbV4W|s0DOkZbV&hLbwLpj1)uuv^7sje;&YlpU3FygF&*W+S_R?7gzwvFx6M zOM^Am!vE(o&NZ;1?18-wUi|O@0sKBr*T{$ElL{x=904=UX4-jb<|lYkD<;mIMW84wV1IUL%4&7UOdsldmao^OXfrdh(M+t<;?tphlzmAuWy1V zVr%d)&}`)fpM$1m8ab@WU6KO_LJb1+w4`MH!rhUkFG1HOS zs_Tb2WNbk!;`c{VlPhy`L#K3WEf;aci^{_mrJ%C|ZnuEBjBo8$oI}WakuJq4fZ%3r z-)e;t*x3(o6UE+O=1Y?7G@5pxW<8YhLkIQ(T&10skQ~O*e;3ixCiy#TYXH-%{3SsYwSQu|^D2JLul`EP@cBSSk2Ulz@hE=WS49{$WMSRA# z#;?3?c?_4p{Ny?o-_m$nIjEjBs84Z z_jn2GN-s&x#S)&U^uwQXMMQtRC2vSxYCbM4dTzGp&(6)wPR;iO-SW|_u$70Aa{2PC zaF(#fn%U-9t7_1*;1tl$roOB!RL?cX@5~#Ln>!@$&Uo`hcLs8E1Gu<@Si2i(4P=k? z7#HZaGM!`ci|*ORKJ4Gy=+1(j5L7&MRomydAnY9-h*7VSD?9ZE~5*>v&8VO^1!?xEP5) zVF3cL&FLBo8wuf|E!LF;KA<*E4cc$>i!2uog{P%0ai#g-G#ijc8EBsxlQdShp#$oD z*7vKaRx1q$Jb{6n!&nU0!Oshd5~5V?I{8ha=%JF5Uq%8Og24>|_#>#jC&xdO!_IwE z0k*vyu4*k|gSY<^kJl=Tf zJ!6G$j>lV4I0_vK&dJYUcUMJR@sjc`7F^3U=7Hjn4%rQXG*HK81cZBw3RWy%nqS8?jfN zHOY*jWT_|hBrg(PD*3rB@znfyOG{jQaY9{PEE3O=I}wW&q>{|`q&}m*VwsiJkFU## zD`kv^vrBEii~zJmW6>Jr-4y{3$h+-8f#cZI#F!ECu)VQkK;Tf`!x&`1G$|!_F@`b; zkxG18daa5U16p{?E41=l>qkpBFKusbiv)T~YkAf1y{)-jxK_>^o;$n_K`!*WgwY&_ zVMqvjU4&V$Vj{>0PJ zkA3o|*v(o74tEH6+y?mYj9@<*Qp7JOlyKx=IR+TfsR0GzOY(UVu&HW+SRjbwDR?8R zhCBaifh{>|V{UeKZgx=Acq|__s^>zz4oM6Tx;x~vJfg-us%K4Q>JJ`S=J{?ZqxknK32zN?$k z*_~|5sQ;ohy3GWtRe5$TORZ3L*RyJ1CH%@6K=wev#JbOuZH3>%M;#2??bNRPUjS!M+kJUD)0e0JAMVR*c3)o0^yRhxhx?MU z`;yA^CG~%@FW}Sd<*%q;8kaSeo-Jw?@pNZ?k?DNodvzt<-lk* zmF6~rXHs-p?Iu#96ik3XzCgjCD^0xdq#~Koq#!-0y3Mtww+h>z*k%ge3nIj5u(3rV zr&YEz%jF>LJ{f>LrWQw9q>ek?Zl~~OL8$YDvK(Iuhd)y}GIg7#R5{p}v@EyGrvy|) zj51vL;KF+PwLqWl7w;LM|4zR?7UwjHQk=fw&USmk9>)PLtTK5M@3J1^bDhh|k#k+X zDriHX45=Cdk@SrNOmq+Qjne&E;KQ_P>VJdZ)AQ@qc+>`n)km1%>6l;*mVAevdUjxr zr=R^CWA`j}Ofl8Asg{IA=sTF>6|f-i{3>Rc4SisBS>w)U0KrhylCZTBVfel<@5~9{ zt&VNH5>Gh}HJQx6odskxisoM7jruN81J!rOF@Dpl^=MwcfL=ZZ zJLWAor>qE;gnIOr*FT4=r z&CszIQd_Te1%ob}TcPN9AK-Nh{88$_3p4)uky~&W)8%GEnF)iFU+=*;zkZ}VX;}ub36E<5g@nABN3`dfprpTi` zW*FM#ms`>yA9F`#g%!rRHrn{{mNc`1TEz%+{*;1{)9=g*rM{9C3i0zU^8#`3C4UyY zmdubT=gc;7!XKQN{MzmkLt@owBa23uVaxB9(U(#`FrHepCg^qgVDt>~e|~)SDF--y z%wxuXCQq##3a0IEz?$3{e~-`O3-5LRT%#U-bHRn zZ>8rOC7#DjDPKGB5Zj=>{dt@$!|n2i%#qW7i>TD+!cX1s756v10gvYiD+kTM`2G~* zyE8q$W1v%?dG_O*jnnWL*jQc4gaCRyj>$x@7r=`{0eN+CRptjIVEgss&C$Ov?+WuDR9fvR1y9!1a z@%_}ZB4|KqS{9iKI>P?RTcHoj$I2Uuvo#ol*=^!e39!nQ>cduu;gAj9##;teY z(Hd0vjxik6WDGpAJ64B%E){@i@pZQMq$bAd>tooJy@I1&4F0l!fxJ}hm0?Z0PyVVd zma1_10JX4r*2QpsPF(|DQe(u6-eBr0NF6E=UZ`NdXe<12vtep*+neEWz%X@Itv52X z5PlUv%*hDaZ*K_Xqo0~#M`8_}I;5#7@gIBi&xI93hgR@qYkBREGAo#WZhp2HdTxF| z@?$Xg?|ax2%1x|F_%j7@p{a>5(1@X>hMG+;EdS!gfG70<6zF$c{KEy~#w{4P6#Go_oF0#}3g=V7 zyU-(l6)Mkhq$+r-VbW1%6}HA4MNti=*V1{K@u7>=@OLz#h;a)FP)G~>u5eD|RExJy z41!bc`$hccQa)4!nXT)+X69%?wdX4r^G8VN_^o`Y7YT=lB#? z2sU8v9ukT*EEmSNc`Qf^Y{#*k2+qO5`%E= zC=8Y-ALCd#khQRGnvywcnE(?vzD8r;f90Go-J>Y<+=s;)~eD5FM9IJG8@RNF3D@-H`ajN4Yy&#b&kh0>7{$P91&zF2R4lgrI`1BdrHe;BB z;EC~Gv)bW#R+z(vBF1-EUVxQ&dOiI|B=M?3c*^FaDGd1Uk-~E}x`Q|fY}cbu;Hm4t z#@B;SLOMHqjpypn;?6> zAdWrG*G;!KzYM2q;yede1nx1;{D0r$45H^FRceX?Cbr+?vcWPh*9Qj5?auZFgb%xN zgv%F!yNyHld19x{<7nP_cXkxO1l|Knp@2D2Un^_^rq({&x!I5YKThdy%0`riXPTxr zpv{cN@iM8at)`~LR~QD?`*$?0t8_$JJUY_?2jh`lVZ1d?t{d7`;|Ue|qo?8dR#=Lr zee~4}IOXYOfg=)AOSFZl8swsnwo0E*PM?15^l*8@Ri8COxvhDjp{=<&hWLVh))=g# zcf(azO^c7KFZy|+u(Y)B)zWB@XXOBdKU)RR z0KtJ{eaxdH?jz%-XUb>7@oSVo*b!!|$Cr(pexqQ?jPizzwy`^3>%RMoyi_DuWEeYu!HID}%=& zrF>KjZi30fB4)XgS%PW|Q6gR|m8MLA2K&hK8b5nl|Bly`lS*Ozu2KvE4v^%-TPBnl zS$c!boW&Z=vmKBn!mU3s!ex7SbmK%`xAw(x9=tNIu+aNqd^Z9+WzKSagTo6#nE)gm zGu3JC$za69*-QU{Ieo3v?A39syk2;Hp>*Sjc1yoH5}*1Xma{+gt}AF+u5sfG>~YY8 zF5#Lw7dwG7{8|>c{)nV z2x|~ctjhE~jx8|`OCM&{u(HVhD$PM(Jr;jkh77AkJFM&$pBpNcaq84coNs7~wUIz- z7hEyk_Iqm7F*4=po4sGI66&SX;{BdV{1dsgrKu*%6Nn&1ZK*g|T6^M{*uiHf!baT> zJqe%d;0G)XJ~LGzKl|ufw zaX6|pdY99Amv~+IN3gzNjPj4jg*FB|1~U2rAP~EjK$ER5a0+#d1nPp&0Pk*vWdez` z-e6>b=fxL21sAi<;PuqHEk56tvfTG9XGLDodqdkAGP;8i3HV0dbC1&tpGi%-7`lTK z`*A?Y6*swDH;I>W%VdQQeuUp@7~~tt+NjacAI!nuwPjqORR1MPjI&RHM1&IDz}duc z{uDF)xU$9{6K+d4nbSc50ElPU7{fvd{@)fkD z+Vg!sU~T_W)(55z@!^QkrC``Tl~P-91)rtpJ6e{PSC;p54E|8?s}kqdM2knnJ38Vc zila3+)3LZL($W$sD|UOui{SC|YN8L~0L54c*F3O_Jzh7CQ-vSagKlp_E(%fq-9b+P z8o3TCl^=k=H*v1mWc*#*K3u8)2qv)Y z_TmChu(IyH>D3s+uWN2O3qiWISqRv--i07NT9@>kK*uqrMzZDvkL;45c3xiPMJV(nF2NoAzK9!o7RY}$i+rjoUEtzvC@tbJLo3N_?;@XoLY+vhqt z*6yGit~{~alQd{2#=D8qXV60H!4#D>HOf2EhL!}zDG^pKe^7@wARQ!|*?9=(S(JHk z;I8@uAr%nD?eiXSK9wzmvkv~fy79FY?#9WLZfAQ{{kU=U1$Cu{yJdc9Wz+aNZ+;{M z2e78f%BGU$5^rv{J25nP98e*389Maq+F3-r~k zRZ#P*3)Is%aJdqPAF2}&85udDi)BCDV_5NR@T=>>8Elt8^0!ZnR?UDn+Q50Z>|TcD zoQ74=4A8fos8n;R*)EW7IuKS|wcJJv{Mw8Y(ZDUn%p~i5L_DT7vb!7Xe>hj!gfh3U z4`<`LT#K#vR4-c3pKH|z4y=|1BAxAr{s{4I_$Z;y|)n` zGurWFQn+S=QD2wrd^*GTQ+hty%CxBW?c z!Mqv35Bzs`c0zMsig_D>^BxuVsE{D{mgkKLB9Nc`*F_KkIL10h3=lb9O9wq`N=IZ< zPtM9Jr5IyQl!15A#BSlwvB^@#VfrlaKbMD-6a71$v#jTw;_~O5PCTg-8&CWqw=A0D zknt10kcK7WmN>U8cPZzzFDlVeTpO&V*1&)9S%1EN2dCPBW276TvBR=<;BO>>mrBZ? z$RC2YirV{gpxUwjPDnzqEfD^7I58Cgp5aj9C*{j-h5m- zsgWA@9dFglB~`<&{71X|A@+9dYrea_;2ujeMS&S~%*wB~s*&m)q*^kwD$)M#l^AD< zakhO4m*VfE*@b<2-eZ)Q+ zH?gb$$&5SFPf0wo%xW^610dBTHsr9QpzTzNi8aK$q9H|a&CMY@!NTnzpS;k2~v{V|L%L4wpChC;K%Ck|}SRj&`=&Wds z+?^LHE)L~!X?f9nPiwp;3a;Phs&AUofU3q$Zc?ue(VE7Njz$!3zm|{|cQ^S;| zdXO~yN0NA}CqK$%{bX<5`0;gnf${NYuAwLAb8>$T`|m`+t-b0m!n(gKz4EVDR6oK* zosTN_s_LrhuIjzhYbD*C?xs5%Nl2P>f(d~PLIXiS zgC-J{B`iT@0tA#0H;9M<6nAFe8$odSoSu=A;-tYzR;FfVUYgy<6rKC zb;PssJ~~F{h#zCB{~RTF1l+SIf`i;qf$y26N#1>d{X7S#Qy{wHv;VUXC4lJr?3<=U z=hVYf_HV1dQ`I%)r-(&tqwvBG6RIdrQR5VQZ$G^bpm9m|ZAP(5w^3c|u zlasTP{NG7Gphwa-W`GC%GobB4no}XbU&q- z?Ob#GHy6V{CO}Rws8#a#=3jz?yvh~-KXn52U!_2 zp`23ki1_VRh!<2qG{`klITn{3m^TyB?<>ieI0h$%NkWDoRrLzf$vl1Wf={QD=o#Gt ztqg8V27LC@@RW=yYlyQYnx{)?7dQ>|!(f4yRJsd)`v@0Mq62zAD0h934*=**x`UDb zAC1bhqJ#xtALE?ScFP;KywNpRAP!WBi|GX2?S{RdoX}n??zM`K9~&+76=n}@7>>>r z54A_3-E_)(;tU2I#1<)9_}M*wpz>hr0-T1p40oCE2`w*z?hyd|9Nm2suYkWR)*O<^ zBGWjAcbleT;vtK0m7z6VXWu$X!o=vl;SF=6qtmSzBUY7X=h*JM1U~zVffz?wFK8r57D&AZO!}8o+8IX#GD@zgXBvfP+zAj=$c&LV4ck2fV@Ys z3Mk6d|57_B*c~_w?%B>1{UDYQo$Tk2$f{$sIW=WJsKg?|TaffH$zjI9sn@d>Li*TzBY^l% zMCGq&C>&W2K2))uSAI|2A3?){)jPRGaKEF6xT=GRozPzxUYvukWljSrz4lS+bV_F^ z*x!?o2`2wY`~(L@p?23k2RVEWkq-nV&S>M>93Y`6-GEAj;Jzpq0_k+Vu7|yYK^6#- zq4IZEWu>|Y<`3Mj757DyMJqj~HK?@`))W@ho)+V*Q~kXdWVJR#wk$!;#o2Y6DDd>R~_ZKr(&sgkR;6 z`SWFOS1+2Lk7TKhYFW6B{-oL+q_aE%-8_pgT8@2A_e6*ZKpvrrEy#5R(7JT0BcFq` z7ETXzggNp&f?E#`sH|&K&xzl09^d0_BaxWhR7ic$a6iO!xg+Qul{~Ss5~XhyFBZgp zP9GFEjkG=FHa6SOrwXwMD0w@u!VeM?tz5t>Vl7G!MP&|9r6y!ezqCm4pYvj%ImA$4 zR~pb9uv4`r76^yWu=W49m4CvYjgO$t60A#d=ZY;SbZ4b|X`IdX3t_RhQNM`TDLMn6 z)5SRzZ-H{nQU?;D0b!` zLKp~z1Zq0LBFUA7(`9{EgL2yW^7|7lp+Gp&x~we{4utxPi?`gFi$?R2csdkHhc?$t zm#7#i=iF1wm;X1P4%J&$I-DsKGT}7pu7%Rsm!F6RU4_Gu^2i((NJERDS&e_lGOX-7$Z zuS}0;VD59c>|%Kt{j_R3H2fW!4+JH9qpU?7Kuj|XfG2-X1zCH3UR>~^5M?0wVM-Wv zhW5hOfrP?Bz(e!nmTyxrHlzq3$JXF?^aXh2r50{k> znG5NEq1=6nHUdRgnhA4s<$Ef^U`5idY*&64wBrJ4=~HKv2^Y37Nyt^12~o-Ovhfo& z>ry<%d{DthlKo9l-6YIG+p35edi6`@XJvsb_`y&`&rtT%%52B~TlPQI(}4t(0>Jvl z;SH@X1U`Zb=KrkDxNj_Dr9Bbj=mi-W0D{p^E_YoI#ARDs6yi z5UUt~m*n&k9Y>LnS81MBD>M@FA-2tXDoSygP=_rBfPXdb(8)*Wq^hhvn1)n7$=w^;D-L2u2_el5oDCxFF#H7@U37 z?WO@aSt=AF^<=uPkrAYvR>^8PVGf1Oi8TZH?%~Ru#CBeoQ=^m$fez|%EdEssDSgL@ z@|yD$U z-4pV=a>~C!FQD&Q51ZWUklR%Hf}wIEP61j#PRbz24u~W2`w$>N@CJmXM9@$G6Oa@~ zGufHu;!h-#27(oGBbMsV_*H8pXGPow3^Lx6ckfO|eF_4Ko~olh=H>glB7F679I zmn*oHWLo!Tvl;J6yEnFH{0V<7bBcaOUill4V+jQr8WV|zkYv$5BojiOJ=1qfVSKTc z(IR7hm!~hHCA@uo-h>wELxrKSGkl_y$5;FPV`-~6<`27yMOWBARxVC@{UZq{>LWGYC-drkfPR`vk(DTQy z{_xdTfB0S)^Q;vlb(IXmAMHp`VO+g1(Uc;%NgN{dUa!@J*kc^>3B9lc8o_Bv-F}pA zdi7#?Ptd#OZZcJO(CiJF){ngO96U_g>!J++?Ft#~Jf_KR`Us?cvT(@EUDDgh*-SnX zgeFrXu=9>t|2$tZ92pt0JQSb}J4ZEbf$|jOw6%LT)Fv>e-MbgCrcd&mGvNuE4fxmw zK$I!QsLwYFSyfIN(I}=1*xFKsXjF>a&X9hBC$4|-DMtKb^idy+8;UQe_jx>hI?oNW zjHyL)aS!_@UvnNNL}M~!q4PLQz6@JM0zM$#E<-4-#?i~T($klr{4gBWeh(G_TF6y| zSk8x+o~hd8mjEV|cL(J1#;~dt-GQ*CqV&Xj5Ne7ea>>5miY!L9nb-Wjg?1U)EtUCp zUL=^ey;QoO+U)ZuEwHsXyrS|h5y*x8c$*weXi`?FSYcaEkR2c~)CxkVk0+DAC?m<+ zcK35-R%qd*ap5PK)YB;cNXzyw_yi)p#?` zT{2<@Vz)@krkMzA8X@LLPvkJ^A%rC&zR@{h3}jsd8^=N7^2E%k1RVRffWpLI)Wu4qU%v{LG< zSk^>k@18-+8_1oNYJ(@v8}9kgQQ-TVrWrm937iO+OTG2LdAGO4U2Zk(J`17FLf1_b zA?A8?bZBI8*Z`tF=xCJvCqb{AgZ8pMt{izwxHpwcG%BXjm{<{wW?K7HTFs?;L)}a7 z&t)rPGnNlh(Ku3`S&4(z7lqeG5cvK&qHqn~h z3bx@?Ux2v`>Lm9G0!kTl`5dz$-wID@DZN+|9z#KsjHFAduC?YGb7v*aJ1;)kb@thr zj)p*37)r>uRO=4=lEB&pCcJL>%m$F95ZGaTK4K)t%bN&yAGRe>S!>!mRBIeQhZQuOnwq$JG^q0OgokLx)RB+fAQ~e@z`BXZl z`_!m^*a`rG+7RZgR90v&FrjZVO#mI9PtTxdZN&@l7{3s?Yfmd*hgLdLw*z|!>kWgn z>IHJ#MrEM?JL8=Si-9yL8gG91Q<`$b>@A{dmxTMF#_k>LT~szHZ&eC%oZk$h=GJz2 zgYubxdH?z3!jUmy?!d<^d$MUfkap>Qz}uS*FH{6`=)GWQN!&F$_|bp> zOe5?GITTpmzmBYhQ!n6^1aFJq5?S9$KdbBQ{cf4xX-`e zG`#hhu}DW~*fbiuW2jJPrrR6NPWjth#d9`peP8*4sC`S6Mue3b&U@Sl&PRBP$C~u{ zm-;-q?jLKuVZ*wWDI@K7H+kCKDv~H^tY3h-BmWyXu}!eyTmq^jpFY}zu>kE##;)RPV@Q;|L(#V>N;!baKRn&sD2e8%Bt#eH;p#A-H58S zP`c0$Tvc~IoY7p3FLSiR8gx@MFb5E(U5D?!Zu92rHY?K@vxX3hGVQuyw6_dh5^HaZ z%?`D+8;0JVc_+ll=I;7%L@^Xk&^_+gz($D4pw9$)P4kbtA-U*kG*mjvergT(&@7(v z2t8t#*oH@JzV1|XTJ;`R#3txTge#0G=Kl?yidUIJ@rECxf>`&T4?42{=}%3kpKt~O zQz+4_>8byJL8lte?N2##`-P0f`KOuP5yMPqN0`@nT8GHXoHGW8FqtB|Md42{vDf`! zX7sFv!4_*+R;;C2cV>9FwcyXB{v?xnc`&~yB`%N!dJ!juP?f%&4cTbQN3l^~My$l8 zh*d6uX9CSwdcH|klakwfku|VBNHy|EqCnF(d(YV%iQMWE;tg1E3wjGFYdF#H=fSby zpEo3ityBRs-+z5}$``yP3Q0eDOVFo0#W4VKkt)1rQTM+zH2h2VqW7QzE~dv1?n^hk z_!1z|5OSCBzVu=PG&s`Dr(t*eF5)`U?0Y?zBoD?EWanbluuPd0U^xV-3OvdIWSRHCLmL&o?M!knj(a*~Jhzb1mWYktm9d?m_*?v}DD<<&E zJ9p#aZMP2L;uW*43hEKCi}CN#CD< zaB7$!Q5?m$(<-b*RaXYt(QDnANTp&WkBc4vH*ooVEy3hetff9k)r+US28;#wVQF|# zlemz2%5XnN()O}MXvgC7e9?ðT-teI;ETSiVhsid_R`bi3p7w2)ysN?E4XsB$FN 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 z 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 twl)u2n}@BcX<>7EASZQhW??5F zOJQDZyGB7eQEig4LF*c4qr?mh809bTI zSad^gaCvfRXJ~W)LqjkiP<3K#X=5NnZ*5^|ZXiTuWNBkzbZKvHAZT=Sa5^t9V{&C- zbZK^FV{dJ3Z*FrgZ*pfZaCKsAX=7w>ZDDC{FM4HiZ!a+}FfYdAz4-tD3>8U4K~#9! z?OF*`Q&$>(f{KU>j>=eDTV-0Mb*Zg$Iy36DmRd(kD?N5<>(tuLv{l=&TAkLmP8YkK zTD97`QAS1Cw?Y!a4jPiM1p;IzAuI`j5D6qe67sU%xe4L%B1)ngdy|d7hPd)d+L4@?{CTn^`K56dJ|QrrJ@t$$ue1kJ zdQ*5QAf+Sa{&AK->Y3DbLZC~pavl|CEg| z5DBD@GD672xU%lzr3vF zWCMBsnALpd`Ak!$gnS@nmZ!=jle5@kG@;IXD@&E7$bQEY^^Vp5)+~_u^cV!6wJ0ky z+md}Y+oLNe=Co`=b*VFX0R_J+>Muy&WS}yH6?S1Nc zdN5@XeK|Fh{$E-mt$Vs(;=m()+EvTS9`B6<5mj(4HLdDQhf03)A?q z3}7%&W|YZr@@R3SuYzvZm#${~u?&=ZW?Eggxg1okqRc8+mjeuNOfOHx$#~mu0OP3b z3XEfe^p~Nwgpur#1Q-d7G6bo3lK}=${xj742a`pN2>ZX4k;~xX{am7lajfDqhIWu% z%MM`dXXrdIwWCa}?5g+>hMQXnaM*<)l^gmc=1-N|ZT}Up-o$>DGAGZ`VpsLTN>jxv z?yr$HGGPNMpTjgV!2lS`Y|n;%UDeKY?$UG6bpK zUIhkFt?08=RoZ18Wd`I6AZMvctE#|zjsU9*yAt?R9cMPWoj`R?^{+6zQ`GV5nil~W#WXeR-9XarEIfY$(n>HSvTgo}(Rs#G`E@0nR zvy`h9*jECqd$~F;YrxLU;Qfg4VO(HGG`GEv?f zdJ0T{#Eo++0q#y75c>&M@vwC;|2yt4IQucdW^BBg$bxRrol58n!p4+yc>JPmXC&*1LR zTCpEa`jY=hA1C(Xf6v>1vz+Wd3;6Os6(sPt+a_wzYp)geS3! zoE75sB}88cw_=vrLW2+$?uXn85l;w&wf&pE!k3{0MCZhQ!ezn=8xPSWyeL%Jxr~TBuFZ5Sfx68iP#+|oBhok6Uv&zXiBJ&_mq51Y9LB#W0yYRUMBNU~J_O4A zMNNIOh$T`xI6TavN9|>A5Lun{Y4-A?-AX{Xq7Kxr15@g>VtYY#@pYii(lFQ2o~P7x z)`_uXBcaV1YVks-|7?){c2L0-;8R!StQB8?=Q72o-AX{_FSd%qVfU&OTf~pot%XM* z26c`N5>bdvYH)mKC6?MGXVgcC?iFhX>j(M0TDNbI7Czy{UE+7$oJ!QUG<*V&*Xlui zZhav<+VGjfw#Ozl9I}s2s~t9Hm>a0K>%{T(pAYhr)wd7wdrG(%mi~;J2{ffO5MX%k zH3A$Q9job*UCOodN1Z%I!n6EYjYgMVCh?qx=Nql{LGC7Sv`G#Vc&G^sK%(qhZZ}oi zWxaeRvC7ME7mq@MB7d)`2 zZ;p^vBS`CtW`F~H_((s-q{ike82_U5Whc*3L0SsS|Dd_MX@&<9kZo!K^2cEc)E0oD z3hD_sS%c&61lE=^_#4*V`WAw;uv(%K&rl1uB?dv{TU!AJlYC}t8&3YMkbE9B8cijCm0e`)pNsG z?gYK6zrt$zrqiI>fOv=0OI7Xad@vRGsNz+?!!WAgsMEPqWmS3eYc17AHLAW2e1UJ* zMK$msdb;2uNxfZd?7HOTk3D+AyA9oOz!yQfKJB)215cnwrhW`Ty2HBB?r7kHc$>Pz zdeELUMF4_mLVM&rz-^Fw7QnLh_n0*HiWvjaKBMW>u)BicWrH;F8mk738Y<1Za6vy! zb5;YiKa4@`_Y)edWotV%J8isX6Xd}#uxKKNwVAb7HE_YGd01PiMYV4tV@n`F7pb%A zh`PnL?G;_Pj-dlXz^Y3fQg6^*MjwP{`e{GX_3CQ0YmmD^`b~PVUZel6+x{)CZXJ47 z-=+sHQI5W^S!heU_wV`!J*xj2Oh@h(0e|!(REc)!zqi$=Z2@XriylVP(OT4i0w<{T z3^dZPOS`D=JpDT;7q#l+^-GbvO<UNY`8{uwO9f9U}H4I$=jrY{XyMium7 znmz_W$oL}=V1C|w!Bl0^n*fBxq%b$wVCt~Tz+7*PHy#)VjmGu;03k4yH!MdiUs^tf qIA{(BkAZo}Bu&yJP0}QdKl(ouPUnCMx<| + + sfizz logo + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + sfizz logo + 2020-05-16 + + + Tobiasz 'unfa' KaroÅ„ + + + + + CC-0 + + + + + + + + + + + + + + + diff --git a/editor/resources/icon_white@2x.png b/editor/resources/icon_white@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e5bc80a228e51e6c09cfa811cef11bd6d437c6b4 GIT binary patch literal 7336 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 literal 0 HcmV?d00001 diff --git a/editor/resources/knob.knob b/editor/resources/knob.knob new file mode 100644 index 0000000000000000000000000000000000000000..093bd66cc323bb5dcd998d9a7fcb57004ed076b7 GIT binary patch literal 22266 zcmeI32~-rR}G$b-n=ts-kGVKW4PaUZ+&&| z?^f0Q*R8Lo&nWL{VKKzo!oornFx4jr{dFV%^|C_$53zimX<^YbKfuRRl@jrE+vOmK z5Zg7IvWK1Bx4AOH`Mx42bMD6Mffr-f51JlmwKCRe^{5_R!#{{!YcIJJTkJOUZ$BLD zc6#-^)f37`|E5pOYP|D_lQ#e8l|7FKPqhE0Xx$&i#!2n+n(#OQzizv~;l*dw!)$EdMAr@Y&~}*Pm)Fj}FJ7FNzx|sH2j^H{9Pyve`mA?-fBYon zfe~pgX_I%Jx^}uIF6-E_WBa~ZY#LeMIDSUptY5x=YI$KlQDTuMaW}-;yQj zn_s9uaN~~#I~&J6->$5=o4$KxjZ5K;-|Gj?zZ3dZ`C;F@Z9i??QP&w>stjrpR`?t1hc{>GqySZQ5hf=Y*z~Or5^3 z&!J*>|7M^3bkcN+w-x8~fq(9xUz=Xi->FK4^{cwH zJ+8^}6T8@!n|Rse$`?GRjXdi>oEmR`SgS}8w_v4h z7WXzq->SN&NP7KNo^l}1vfAoT`^rm?-+6yf;YzJ4Z)+WqV87h3G{4+?OVA_rl7=UW z$sx&J`|zi-xbz6OHH-FN@@tfBtoKcDc0ZHb^u*!4l7X6}kQYH|A!#`eB%<)3hb?QW zcklM-S2eyN$st09wcWb5+GbYOTAS?bbwQ(cMAe+Tx9Owdv)>Sz4uiLD-k|-i%pu7^ zS=GxXVs=c#s$u=>{C_F)%5d^&JyIbm2$s9uIlpOb(6Jc>m#u1JtNkYIxZP)PSV-9( z-&ZF4f}pPhh76fGyWw8z#>$5M9v`aR%Cyl=4#t$2Q?jb5+bcJSuC~3hZ)>`={*{GN zH#+f!sVX&MM&x%rzMLi-b+6m5)MIyC`fS;n{Y~mjnWkHwon+UTveChxjA)BAtT=aL zdC6_9Z$ZeQqqy6EO36@FLyMu}wMWtFVGgcW{ce@^T^8jsvEcN4t@F(+(bZn{sj2S& zF-BLupm=G?oa$ot(p&L8oJ*Y?JiED`A2_GgB59h_fRL2NX_1@GVI$V{EGMET4{fUU zn_FyAWcy^>sFrCdmdmumy;@u6A^I1_jji}d8+3S3VY6y)aB<$XtweBROL+a)3G`*zxeCF&-d@W_(>Hnkg4KX4Sy$#9z5H_k52BCEOk8RZ$v zpUNB(cPu4>*A&f?U2?DWuCv&&cJig$QIak16|PGd|J}+5#KL%c){1E#;PJy)-ujkt$!G|Nxd=MZKBKa@9-i0uGFrn_FH3@ zk`g9c(XaJQRdeqd6|oh5-WTF+xAs`(K-9;zNM87RcTaD>c|Et~PW1`xyjk|K5qqw0 zz4*!TnBbq5ttSf32J3(J&fXH+l4aBFX75wk;l)Re%o|j?tnqF}ZIf@%+CI ziP)Uo>siCg_4~_n!oL08t>=k;Irg7Rr%V~S>Uq=i%Wm-vWZTD^(Ft>}{gzv&^stkL zjsI&-PR`?dxMk8XM@e{4rM~9IihIAlz8}(mzK0*;Y+1V@ePD`bcUR(WL-Nh&3Xf-U zS;di`hu6+M*I4G9J9X5}AgoDMF0aBSS!XysxvHt%4Ib~(umLsq4|2+BG-SpAz4u0cGr&&0>Og*``Bv1Rr@1+Co-hcE}&mXfd z4&JdpN;{@%iFI>SpI44eV_()@ALk%iw#$F^l?zW3gEo~LmK+XKKhIkfez?aM^HOqG z)Yr|p&|Op-u*+`q_N}$qnMYbEfAO26XA5sT6I)VqwQdu=WkK~*;y;?{Hop$xxkp6L zE55HAh?Tn^7g^8yrS0O?3obwRubq1&IVt~)*t_CKtVl9DK8(_dgFG)tK5n z!ug4ZBGzqZ?VYBkD};}7+sn_s7hUN7Yq4};klmo-H?`Xi&e+=vU7kJD-2ScG#lC$O zZk6x#U!8jD#xJUKs#ORGe|p^Gl&G-Z(Vn4a4wglU&X4+(_~x;DyY$qB2iY0j zJg)tv$DmgQIcT}I2=JZhbHFQd)q>duBG$MNb(1g{HF}*=j8AeCSqG|9k*$=8ti2Nr z20~||97?gsI@pw|C5&W=si7Q7bj++oQ$nICShJJ}RpJVsEjDw6ncGZ*B*GA^j!)2P zbaCVvf(TsXKwc8aOTyzN>BI{Myl}`1hrDpe3x~XL$P0(OaL5aX zyl}`1hrDpe3x~XL$P0(OaL5aXyl}`1hrDpe3x~XL9xuETFDc+9g}kJYmlX1nLS9nH zOA2{OAulQ9C561Cke3wll0sfm$V&=&Ng*#Oz^s@{&ScQXVg9CtfnZO9pw# zATJr@C4;z>#dC4I!IpigWyyTFV9P*Mw zUUJAw4tdFWyyTsDDF818~iGycCd^0`gKoUJA%d z0eLAPF9qbKfV>otmjd!q@OUX8uLYD!6(cn}6G=uT5@C96kWwrmWvpK7k4i`qqLLRX zNk}!RiPxAkNrci{V~Exgej2To_L3!1(2*@XjR{1wN$EBbNf^=&(ZrY*BSpj7LHa~p zOh*Gmz#fIv1kY{|92I+U$Av7L(5TKLDm#9@6Kt}^Laf?kp8iR5IXokc+h~%Z$qLI^B z0+9UHM6(vzRZ5#fhtfqndD5gY3}PhMm1?baf@v`^K@+Xl85zGuOU@igI(V@VZ;XD&fdOwYcoSy)liO^~_p4x=PYW7Ub%K3Il+L64j}AZCiw=v6$yf^hT8Zs=_dcGQqldxl>H6r7^+9~oRHO| zi@EkmGuWgypfOJnAQod<~;n2nP1bTv$>0))~h zz=8UaqbF;r0(ByKmKYl$Vjc*T!%JgAPch~{bZ2r8YI;RsZ8Rv_!QOvtJ9Hwp4c80G zL2kRyPYSXrviKKVM;j&pYQYbHVEgz|k6ZQxK5QpA$Gf`aLjvGhadFWi5Tj3~pITxi z^)Q7ljCL_Ygf(P1Xq!d!A#_HKDV4oHsk1OX{;25~OjSov#$g_nUbDr_I?5bNJ2A$| z>npcjvz=ueW^vg|oeoG}>jZmIu+Red=mX5r2fPb?07M_aojL&GW7zY2eE7U7G9u4m z9+l6ZY1-uk@asTe^C@5(^5=l>HJb!}C5%l6;d;$7_T@A7HD~PmF2=r)u`d^QszyuR zYwYz+>qMT@tjD}MGA7Sq9+yv4NS&c2l7>O;#X%z<*HYf4^ zj=IKKIs1Q0TvN|hQrWUslc251w;}%XR?ybyg_K+h$xkr!b3xG7MicjB>k!H8+Rce= z{)N=hki^+xaghXVjku7~Q_$ArdLw9S@-_i-n=kKR4Ru3-mfS5xH{z=fp3m$Sg|fRN zsJY}|BJgz`&uJ!-cy%DInW&?i3EDcD0Zx))6*2vK#7!*g=Fb7&Yc>h|N*LR08dx>t zD`;zK(U<(Uw6(B%ZJ;+V1c6P8YP6Au(H|+8{c8!_i={<2+9Rh5++9xYH=A!@i~m(O zu;~E-nH<*u>gH7(|3p?2G&4;^__>!6Zioo{@DjTIG&~GRF_5L5+ z!N$#0HYP@Lh)NS-OI}K1V@bOmjShgb}ok2ni!>-mQ=@ zf-VQRyDcP)bY!p#$=I~^77|7{>CNm~GuuV{?dpGOD_bUrY_uWA=8XS?zj)-16GS%p z;3TJT2wU0Y8#B{hW2~KZ^3xhfL<3cV^x^(NI%nKFo1=&z?QZ zDmRq0!0#e^_8e$G$N;`#^KDJtvq$K;ijtxZ1~r%1=W<;6Wz*z?)MPSzDmbnO8!w64 zQvfO6d+f&PL-q%Y7b^FgJ!1{+RYck`pnC(~du`(##^T ztDSH^@0{VSMIYuu$N{o6}5fneO+-1=<*uOrz6vf{Y zQ1fhLNg@9gM`C3!(#*!J91bUF0q55++)qzm}$zL z6Oq4X~m+)?<%67pIacb{6w0d{$pdcMWex zuw|&TIKi(x5mG1nt#wq|Q~W|={b1k-8OOq__%NH&8Z?52lXZpi(6ko*q&dtrW`WC(B>h_5G9w%k; z?johsVT6^e-<_99dUDAvGtVJlLp)^Oe(L9G4@$X}yuGi4#rDk1gkx!*jQfw6tSrIR zDz>3mQdO9oq!Zbq6kANI?eMNxHcz@ZeoOyicHS_z+h)TP?s@&-c_9VAt-{k}cqNgs z$-aO@&RTB)42`2ba2s`Ol>tvUJSlp@TysgxLcXH@Fy#eJW{I4&Y`*>d%{jMw*k@)` zsd=fZR1xGV57tAnI{82N&4aT(c_X0MGb5@>PdDqY=JzOwSrC#+%e)}E2} z6XjTKJ7e;gnHaRe(&65~CiTZ-X{U-SWx=pH=%rSBYbsBxf^JNJf}NSA)&)}oJ(xN- zhv6G>bnB)>0PU76Wx84Y0GZ$FqrVmTb++xRmf`Cv7wqT!LYpkdKJ)6jP&3z))jiU? zzXdY5$%!1dk~g8*HZ@n8vE^>+-1Rl{e7p>=dG^eDO34LgLhB9}J4If}M%xv~ug+0O zdZ%&;k_uPkvKaz5I)?+g=8X=;$CBEg5VeOreq^=aNGe-Uy48Ykt<(BazBEm{yltNzMC`l5)7<&T>r6=Bm2F7Imym zVP`X0JSWWwUees6kI0J3&7E=dsgib@c@b}FC~#+@1RE0ec{A&MhmGILGq+{B7{fpF zt|n?N!f)?a`2E0+CNxacT2Ez6-l+6*kGHraiD*liWc-rl)77=mmaMQ-S!<;KjvYH* zt$o;cqGpHI<~N+XgXSpy`t~`|nc|zP<)D?<%XEaL9k;RinqMuA!N_lG*3=_PdCSl0 z-0owy@vSh?g-n95!nV=IzZCDfyL?pBM`n_T$g z-&o#9=?zz_ToY3-5WF){Zr$#g_HD|GW3>@qsy}R*RKaZWjzolB>rCAJ*}+?wt7ta? zwQM+&)7o+57GB5AWGKH=C~II*A>TUH;9S^-O$65lj(f%0fmbY95?+;)1{13&O&QqN zf3K)NE#w@>DyiXVpqhAT3W-sbWomDN?@2j^uTkuBPusU-hmLiaoEwU@@Ihl&*#qaPk(P$h^vJ}8;#;_@+=}pakBeSpDs->Ak2?pc$DqRuyPeC zb$c->{;n|kcfDIDlduD2#|RpEXKlt!xByn|B{JC8I4l6E-W=8Blvxs_lNgn5iBrn8*?qX36Qy*p^?%Qf?)V4hd`V zHnv+P47tD>#?g1kPnax44PR-@(!%? zRQ}}BsM_*L)jjc5Hs5=gpuywbTS!4Vktw-^(8N6m0EGrt->w!C2fn(;O^D2|g*<}# zzZ!=SG|CU4GTDElKXP>xcYYEwiu(?}J_>U{ML1YH?m=aq9%3M{Vt7n}Mh{Eo?MJ-G z!A@$v{m63}ld>taAgvw6>)8Q-jI3OqcNlAyjZjs@q_kcTEq~@g9y2^A)m9ggfZ^#U z+Jtl8;&SXz7%{Hl5!yxW1ZrxUa-n4MZpr00Bjsp6>-H0{BwFDTGD@uD0|Di8b>DSL zdT$x=VsbF zu8ra52P@x%C(52$Ry0q_jP5FlzC+1#s24WXLER|5>A(5uu5r!_@=)|7P*TJ}gw6{G zw);u?+yspR;RQ2(Izpm4!^gqf&-V!G9LtCp7p61kv;gflgpR`_s1T0+$TEQDa4m6n9Z#T@>3wc>S**uHfx;dmuNbmitS;+aB*x&N>zGm++x6n9 zF!+1=UxY!=T=a+agfhhu@n&P9a44RDNx7x?qRh#V>VMV+pphGgIDk;feO|qGb;S70 z7yE=5ow8(PUe7Lj?8U}Tidx$4iRxsy#t6+Fs$P-6q z-Rd~Z>cv=40t3BwDpiFf&r<-eenXf`AfyE89K3b_^{GGwAa@i42KCwr49~%v|I9;9 zewk6+ZGc}evsAval=$U62%SiWeW-(gy0k$tG)&(g+T2BueQ8FPJ5uLD##MV#a1|~) z;UaBl;_Wz(#2@U5(A15!A;u$tlMTr_bDEP3>sLFrqdm9|HXp452sc7Z$_VGxrXRSi z{A`Nc`UH?)Csz6&RuGMEOCKl`ZMz>OhA=n7Q4I}@J`3({?#a<0DTHh0 zd$p4z+pu1)lLR&xox6)j%B|23vbkT1hEkWE;t()|lKpe}$>VcKxk;~mV-au3ss&DQ z5uvImf<|%M2*F3&-a*c__~wJ*c$(G{5?6jnSOzhU2G9mTLk<%(lE~PJI>_=M!yO_3G( zDI_v0iKq;d-R}`6rM~I>9oM~(mobb`oKkeWmyT1wRbusvIAX z147OcMuCZGlTG+#u1(<7JQ9wQS5l!L*K~*7P7BZ#^bHiz+3t!w*<pWgG;ve)ti6k^xu41*QH=P=)p&lyv!v`t;>T?q?CpwZ9PBJw+#Pg`%ahbYW(MJ9K+D#sF3a`>9t#Zjl(~@~aO7A$44kWqz z`rR4d*cES4c#lvy_K5Fz1EkOW4hpP5fGvHU&^>7#5WVJ&CMG)9ZvAERuI=!L%r*q# zdtSphI3GfVZj=g2A*ge!=+5?D>or|=&w9n4^5I2`OX;d1$yw+8q0;V!1__;TCSNAq zeiY3+;*IE}+7$}2fx(I5soMJG4smzIi&~+qt?iUf_T-clb=*a{!>|YEMbSxXSPIg$ z?#sn5kTFk^Nk+?`T2h7OnUO@G@%^6sPoN@A%wY{iMmPsE2t+?sDBcO@&rBY~)ntz9azU2Xqb`)0_7FIHlW>zoC zg8Fo35vfOUj&g2fAZX}};yCf_7#=vCpE~CBg6wb{(2y-#IuV4XNLStCUaun=m15t#>oqR|)D=Z1jdtXyi zzr9w$y*MBOKn`-SS7A3^ih!98=z1xi%!&$L-l0L;avKgm zGb&uBao@7|lDPaGN9F4p7(oy_!0Nfw*fiihSSAVtw0}~WpTF}FZ^=xD+=hQmca+V_ zdUmF)6oRGBgxtzjL%{GDC42lS9~hvaQhzSShtTi8AzobDbrKAONO6SlpP)r9sf2vI z9$#t+EYMTnX24RH-POA%z3+g67J}v5MHoyo1{;X|IB-3DV38}+Q9f=!w?zQ1|FDIe zZ!F{AO_!U#)$0A@S2N@Y2aLBry*XCS_r``p~!VDV448#L6yQOUrf2 zlkYeI%6;wS1E-3ExjeU*4Q@}&6?`MFOa(G99vH(>S3mhHtK3873+L!xAiKax9YyYn z7@i?}n}|NYJ@$qtC=-)7*POJW7qE<#zG0sb`FHhgoyY1S)ABp(Ic>UEI}R=Fn{rIB z8g{L#AP!jO9{5IoT+KzWtmcZsW!CMmK`+N1b{LxCG}i3DgiP1_EZo-Ga@8>gOKD*F z|BvF9_7YO68=XChMs`U_GDz81*-)mhza-CT&p%&8>R_8?6>@jdZ1N3jD(6MOJ2|OF zC7OtXMB=wIKM)xQt) zU%lsfJw3ZG3Lw%dnLmD^&cL`uF%G{hIP4D1n;h$|L(Hf``RHh8WKyRPGw9eW)Ap zF}n_&ksd+T&NklXZ8f61$Lm1dh5~^dk5n1ODcRiti^l*wVS}P)-G_8{6nQ@i3??t# zl!u&OM6$jB9>^IuNkL$W2$4az39MDm)815F*e_>&@b`rzkvOPw!2}uiX|wjzyIN+D zM!J$F+2`Q zBe8L!3(|di5e)}W0-?JRiy6c70Hycm*5Db7Vvh&|&5bN348nx_jS06Xze9LRsqL+Gt8kDVuAkqj(-IGPNs#$2~zT+xE7JdV3kho z`kjZ+=f4^@x|UO-_cnbHfJ@c1V-*Ce;>&PRcQd7&P0l6e z1@tSU|9x#X;P-s423-26GVO3RvGH+|4r^?j-@M5e%q! z;-3l-unrcHP5gq~Fc4S($1Z{So023f?`E~L|E@z(`HrjhEA=U)P$yp6%+Bb5n083t z)+)o3K)Vqx9Mu4MK*Ll4;_0{ONf&-k@wyeRSSo4 zHL2z`Rw&|6z^(vlO#nMyvU5YVdGpa|az3i^c?rEXvYLBNL9RPKmTp}c4SG9dtZUT9 zZ3S8sx=A?iBmV{LgUaclV8b)JmU>w-Qt0R2YUoY2lREA*SA~G$tNZ|x0F5Z0~Z0Je&l6NCw#yiwd>w1>xTFsy~BPzraOUAoaYRmbudQICvE)t+a3G#y9l8}+s3!xUCVam1f{ z2@Q$QxN)tfqnLMXQZR7vY&i5QI9TZIUo2qu`Jn~&oCPuQSR-Vb=#e~ooV+r_HN{4! zoZ5awv0SQO$``6Dh!burNvanSY0A{1PzspWD8>2m-S$zj56iiBlZBuco44x@FO=Dk>&Bf0^zUjjrGMp zb#MQ)7Df!-eer&&hhjFA`YFBwI@!jj)1?R7 zPscv_QY4pEu6+i}BPYPk$pxB$ibBtG5@3?%m6njwl#h5LFq;g4L88~C1{BWl5F)ur z$xPgL%a%G>PM$jE!q35Z)*t$H$kO*Sll;*=Wfkwu5@bk4?q@JOaua+F=WVv5hP*nd zocChQtC5(L&=^GEvr~uME!Rimv%ty$E}_kvC;ScF%{R}OGZI*#n3VGsyC<1ZTpGBz zfj|qO9mPEzW&BGPCp{xH$3R2yC45QHkS~Vlk*X}i02qOnnRy13zwt&1EE#Z+3|uGl z^Yb%7=X-GKzO~)i1mAWs4}g--c{hSV^)%mph3ub!^#kJio6P!KlY

BA(xO6jz4z{UdPwKcHf2dsn#I zVgFTCipRA@B&j(cWvXj3>8We0tcBt(9uUj=rTqQPKn98fniM*mTq`Cr$H z%!iEOZqpSe?;uLR-WQ+vN}pm}Tp&kmLsHMuad+PfG;TOo`(?o&?{s&!JzTKjyc7mdb^xhhq^xz^2EhES`P;pnv<@|)Xc*955dSxv)<5=fH|rovbVHd5BJ>~S(OEEr@fqs< z5~U@Y-v0ch(~j$kZT(>WsBwlq_h0Xkp9*TH@{$Owy1sUFuWIY*E(B0{2~C}0#K(g9 ziktu9=o)ng6|4vD4dCkFwYblE{)IIvIJ+gRqf8Elt932gQ&Ku zW&g|z7m163|ym0UMh5v>C?G2>jCDy0qo8OJX49 z{I9lYI_wDZc)Egq$EFn1+{Ok4mk5!0nYOzON92mY8O!BJg^Cs zcexp5FK3&35EYz0K=mi|;Z9u=02fSffcRBwCH<;GYN5 zgFTImJv!DofG>)@IS!2BEjciJmU_uEWV3(ds zG?_d-WK@5@{@bg|026Nwi{{FCWaTQHX+%sPsQM{Bo6uxSe`p9g`YS;K=!zLw^Ca-; zfI9k7^G3t&jq=`bzHYN>GyP_#34*Yl7C7|M%K(jM2f&@=>wmi2y@2hu3!Hy8OryBN zU+s6VdJ=zh^>q2YS`3;BJg3M%Hb&&Yd7a_ZAxVQ?(!SbEm+c>2rbz!&pcQR{(fv?9J_MV$BBr^|Rm zj{(uy`Sm=pBUNla2Z=uknyTjVUFTLdgtYO#e_zMCcM9+2wB?Yha|<*I1Yolf#h zGh}OpXifxp!4!UuY*p#xGz3Yb7 zfO~af1tRJ>Ii;1N7#`_cGAE2bv7Zv%2HoDwZ4c!c^NdtXO5L+ZwQHD}-&qNfL;yrl z)m_?=mcB!DhbJraZ=Q1h6(#=W!o6Pxjf#u!`^R&x9ut^V)1#m!R4;gv#0%cTujG14 z5q*l)#6dMM-FTN2kv*3q&uM4Io3*bFaL5Hx^lcV%ISX0)ah!@-#-@Oe%7Ad2(N=se z8Jfys<2>f6xp7+-(b-SB_;j}GtFJVT&`uqEvBGgO@nkj=%G%uf&RlMuTAaciZ8nbl z%P8yfBC_%lOmxyqMtc;n?Zzgdg%FSo=of0>QIOK;1(LDyXh?gap)S_l?2X7`(! zQJ>C&F8o&yvu_c3U>cmpz)>uIx7|O7NilA1xi4rb4;oR9c;K${m-F^1UP+ar^P*gM zVScw=(K(A?*BcfQP?JTNhp1A27a`G2iHri<55j93hkMr(EWr$%DcW3-yvcA%oB#~y8Ai|;^;1r{)10^R@y8#7veg@ChPEBKw?HQq%TsZeF_7`9WGecd!t3QHlenoaS?W-gz?9-b zPkby#%jG_m^9UUieZ-C9h9w`v(~sg?XK`kZnwvw>~iIFAsQkP&>U4CSf= zt*&8$cID4GXw!qae!o|un3SSPuWL^QRYA7*I)cWwPT(` jfRz8I^YP)-9mYGY$4Wv})hxh=RQIS{Q&Y-A-u3@KY&Vc| literal 0 HcmV?d00001 diff --git a/editor/resources/knob48@2x.png b/editor/resources/knob48@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2f8d49e67d3ca65f4cf0ccc8ac447557fc08129f GIT binary patch literal 109207 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

jLW!z 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 literal 0 HcmV?d00001 diff --git a/editor/resources/logo_text.png b/editor/resources/logo_text.png new file mode 100644 index 0000000000000000000000000000000000000000..d683e44e1a582f68ca4ce15119afafea6b358c49 GIT binary patch literal 5414 zcmcgw^;;9(_a_8IBt%-eJBEOCZFGkN6p$DpF&Hr#Q3;VwDNzuVni6Bu4Wk96Q-*{v zCLl3JeDVGLA3pbaZan9_&g-7@!@bXa-B)HNy41JWZV?d?QS0jgEr^JS)2`QNH*Z{z z0*JZs>xt4&&o+RFh^FUXCmw))a=kXP27+t?Eun6Ka3>gq2o8r!dir_=xH$PiB%v_( zf;|`>i zlMuI|6Aaq-vXXbzHG52p=-I0bq_eYveXeIP^tX@InZw48*De2Usfh?xsFa(<)>hTNE0SH8t12!q3lBoj2&3mnWX- zT4w^V6&!RO2j;wqlm&ry!aUU*_KH0~hMr}s7PS#bvKleoui+xoY)CD8DSxS(Ow~G7 z?}SNYwBC&1wT|ip6~|la3uRfmFIuivX*UW9g_|nlEk?^HNhR0C{qNiJ#J96!ic8GF zjw{G=!u!2tC)L@uiOa911MA1k38plS);t@lpWc_2UOeg;ei2m7GPP5@J>J+Bi|gu~fVnu#1b<4# z*Yp^RsP7?FJABYfn~y~4Lj4%NWlS`qvhklAdTwj@xOnb!#KTXd=gY1-17J~7BmK7ZxigP5%NAZ>Sbp)-b`ik z4gC`5b>|*YmSRVKPe^`{iZkgmz~>9LkDo@)ycK#13u2}#b<}u#fDGrLDDG_;s6>&W7h*n~e0}gV z+M=Z_2mk(ZUVY}Y)iexSeA+fWb+g6wF}6tCxW#eek4-B{MI#yd9@(1HfsL0V!b$6| zQPAtPy?-D*GIC97ABXnXy0{6aFZ@fc) znaOUJNCN?Kn&q{9a^wh266%6fx5!9Rc7q*)Oy%xYrY=H>?Ju4^>EQbfwranPFcz>= z(`Qb2=?}B&l1S?eDb=w0lu#LlNTi$&+EcA+q=dUpeXbmBs7&@~Dtu}_CmB_!vQNb? zy(~<3^75fN56`Z^p16-?Wg)!^)Vkp5FKb|8VIm2!)vfdL49Xdk6J{Gm2)&B!6uvT` zE!E?Z9&sa#J{*wTa@_R^+65OLpf^4JMPn4*!?L_#<^@{w$MWdham8; zd)X#?uqyAX$NuG6{8g3`Y49cq+V9~`WR)6~-}^snhm+&w)-u02TpWPesXDWA_Y$x}zynYW4mEe)->SI~QJPHg zRKdNKmRX+4KTy`gMK55?DFRE>faHi7f>5A$^8w@+#gJ_uz4eMJ{VmrQl{*nphJ-2N z2MqQ)DFlts6j$6`eOy-^n-#IX%ZH0+MgGVM`k#-Acd|?m9OfuaAwmy%9CZtH$Aro_SMpcG zXLSR38paIA{<`g9H38nyU4`KzM7~2~_JL$1UB~9m<=4h%HVpz83xp#~x8CqwkM3iN zsg4dpyo<%0jwoo@VgJYoyI(kyxr|%*lAd7@BQ6F}nX4Q4x$_)&X=4x`%Q4`d)BKg@ zvq8!Mb?=DH_U19LX<2|deR*Z}z?}H0A>5D>h+(XoUt7C@0A&v-38f&iRWPTa>Pd57=(%|zE*r)>I3$^(>6?U<-OIKY4E(PLA#*k zU(u#J$o@Gsp0=xm-4lKh@GOgInJ31NyeVEi$Jt5Uz^panO-%#lYfvTDzJ2qHJi6_0 zo3dV2H#E1^Ed_WSo&f+e%D(lc>UR={`VXxA#W615p&37P7sJs)`cD3E#2-l^JI)A^ ze;(6Rg)$JaB!P9tXk1rmT^Y-b&I zd0XQR^3(8dnG%o*cfv%z2sn>oQcF*x%xTDUoXf}cL}|k(cto4~HVquW0TT`|NG><7 z?W>8zysqS0m}wE|<=jVJ7>hZl#jk;QIpnYbrN!#AV!fVYHOvi!LNiXTpRyc7Y;J1+KJ!P)dq zWMj_?Rr?S6fUVsWt z+L;Bq4~p=nHZ35v&yWkQ?(GOC8X-EG4yK4Nw#GWxdiJ(;(j~bMCo!$;>rLw0nfWJ9 zoQD>UHohZ<^qArkigw-hFQYdOnC{looz+=fZo4+nY%20|ZLl503B!k9q5ahdQ)*Sb zoE*Hq!B7I=&b9VVoVp{QcqHeBa)L_ThONYkS^5ycv1una*A@T#V62T5FR`YhxL7y2 zcJN*=PMr1^;Y7nJ0V0LkOeN!VT-j7&XMlsvSbXL*`?t)SpnG|I5}z~5@CPBStyxU_i7mPAn&KU>UlfS@? z^C?mV9B)c<1xgdxR#L1LM}eusz){hvmwsO#%?kWxbbU-N9t)chKVg^*7j~zC*>$7r z<~l~^FqY}9McKc!VfKSK33&S#2BjA)I4#XP%8~>6ef%f42jQuB#g()jf4Mr_hv`Pc z?firn-Wy1o^^mq$ljAm^WA}32SbI7yjc>gRXXJA1E6Qn$8nPEUOxneMK&^x8H1rzz z2#DP%DZ}2pfX3Wy0u8EId(Bs}zIY?my7#C_LWKYVFhSo48alj>zrHnX8}xn>rOB!8 zj8q%jBE&apYNG zO%D!V7;U_#f#if1K6x0$#p?Ak*C*(jVQc z(X+)}KU|NcH37N%3+}hV1C%f%&M%{Ujh*sxYO@P3>Jxv=kg{h26>4C(Q#egN_>co|u{g8Xno>v*s>q18=!_uUZj+L@%gS~eE}(RX z-81B0DovgkiCmm^q%0~ONFCH1D5M3@oL-=*Z3jMk8>sX2 zbr|g+TM71XZLe_hB!J`WeEWVIATli%_$P86MNR2VR zTkGsj0}Jxbfy0q|0CB7>0EB`V=ib9LTpu^sd4K}}b^&^!ws@4yc3 z{*A-Ejq;U@L#*j|Ci~8z9B;?cT} zqNV$9zBaC&)C#gg#q|<~e#?WZrwZTBgGIWJ*=m6wZo@blPbVKaTMZTTCd6-w;)Te% zjZ7_QEamYK+S4^Sv-R zU|TW>!x@`s(-`(jFQD7O(2`W>vwNb+6wzCd568m3@FD;cJ=T|5uA?DOhk2%J*ilEp z!8j?4l~!P0;PV9Q2h{MlIQE>x{XVYPaP-&Zts@jqe=!STkj!c>Z2r`;r07fhMcnJ$ zowTi~pcbb+Kko!Zwz14st=N9zNdgM}q+;vdZSN!p`NH(ApTn1D>^*ibUVk5e!?vv( zkM2L#ZD2UAAL$Rk)119-6;5^SVP0oP-3tf4Qt+~DaH8058}=)a#yzwmg>E}e$RY~Q zlFuAj+&$egzYo#fv`2t@HBzKM&53%G1VD4)blDvZdhZ0o1;+0vfQo&aloRa}# z3MHSW_*3_u`fA0z)3DO}(T{lTV$rZzuio8Pvw@nKd&!7~M8u!gw~El|o3km4n#A-f zlCGza{1i0PT_ijwZBrP-faBI2ejcwfpbC5zi_Xmjv)*f%adSq!VkI2Tw%Z5-wVT78 z@2UjTB&&8}f2sOszK$$XA3LpBW=MFq_35=GckLKaXsN~`@RmhN_ zKvSVGJ1K7mr)FZr3vfpg;yZzrb-Yi6u1HqnS|m&X(LW?6d`@^8w0ru3{I#l4Q8FCo zm(I+T1?EgLYr1CK!gO4(GB2$*1iDdSQW)AqgVea!exHLFm z!U{yd(=;4MNnkt$IakP^eAQ?SpmJ3|-XPM@PnvunmsHC9PnYHiOeB5Giw1fVnU##M zGAFnZ|5KYw-)$l*-tcZpulp(r?0x~8QMTDrcS7>+4#JW1rKR8RDW z=mvHIO|N*T4hQT?RD#zO9i+SR3Y9{oVNES7LkZ1+0KUJ5y|hMtykJnBQyX31Rq|n4 z0w^7E72#Y>W0I_U-r3uZH%V0atKF^66xvF!wiaE*r$FE553Hh7m+qpxio9{}{Uw^2 zm-DtGYdf#AJ#gjI)xC-sVZJpA!TXkXdC3=PfEVYXonlTantxHD3Zfn|X19{f7!4tST%PktH zzooF8r8{eAAlXfxB5?ctOl$;4x5_srNV~gw9ckwcoe??8jPQ + + sfizz logo + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + sfizz logo + 2020-05-16 + + + Tobiasz 'unfa' KaroÅ„ + + + + + CC-0 + + + + + + + + + + + + + + + diff --git a/editor/resources/logo_text@2x.png b/editor/resources/logo_text@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c3e28c637e5168d34c6262c2ed284a4871e6cc06 GIT binary patch literal 10935 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 z +#include +#include #include #include @@ -18,19 +21,20 @@ using namespace VSTGUI; -const int Editor::viewWidth { 482 }; -const int Editor::viewHeight { 225 }; +const int Editor::viewWidth { 800 }; +const int Editor::viewHeight { 475 }; struct Editor::Impl : EditorController::Receiver, IControlListener { EditorController* ctrl_ = nullptr; CFrame* frame_ = nullptr; - SharedPointer view_; + SharedPointer mainView_; + + std::string currentSfzFile_; enum { kPanelGeneral, - // kPanelControls, + kPanelControls, kPanelSettings, - kPanelTuning, kPanelInfo, kNumPanels, }; @@ -40,6 +44,7 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { enum { kTagLoadSfzFile, + kTagEditSfzFile, kTagSetVolume, kTagSetNumVoices, kTagSetOversampling, @@ -54,19 +59,21 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { CTextLabel* sfzFileLabel_ = nullptr; CTextLabel* scalaFileLabel_ = nullptr; - CSliderBase *volumeSlider_ = nullptr; + CTextButton* scalaFileButton_ = nullptr; + CControl *volumeSlider_ = nullptr; CTextLabel* volumeLabel_ = nullptr; - CSliderBase *numVoicesSlider_ = nullptr; + SValueMenu *numVoicesSlider_ = nullptr; CTextLabel* numVoicesLabel_ = nullptr; - CSliderBase *oversamplingSlider_ = nullptr; + SValueMenu *oversamplingSlider_ = nullptr; CTextLabel* oversamplingLabel_ = nullptr; - CSliderBase *preloadSizeSlider_ = nullptr; + SValueMenu *preloadSizeSlider_ = nullptr; CTextLabel* preloadSizeLabel_ = nullptr; - CSliderBase *scalaRootKeySlider_ = nullptr; + SValueMenu *scalaRootKeySlider_ = nullptr; + SValueMenu *scalaRootOctaveSlider_ = nullptr; CTextLabel* scalaRootKeyLabel_ = nullptr; - CSliderBase *tuningFrequencySlider_ = nullptr; + SValueMenu *tuningFrequencySlider_ = nullptr; CTextLabel* tuningFrequencyLabel_ = nullptr; - CSliderBase *stretchedTuningSlider_ = nullptr; + CControl *stretchedTuningSlider_ = nullptr; CTextLabel* stretchedTuningLabel_ = nullptr; CTextLabel* infoCurvesLabel_ = nullptr; @@ -76,6 +83,8 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { CTextLabel* infoSamplesLabel_ = nullptr; CTextLabel* infoVoicesLabel_ = nullptr; + CTextLabel* memoryLabel_ = nullptr; + void uiReceiveValue(EditId id, const EditValue& v) override; void createFrameContents(); @@ -90,11 +99,16 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { } void chooseSfzFile(); + void changeSfzFile(const std::string& filePath); void chooseScalaFile(); + void changeScalaFile(const std::string& filePath); + + static absl::string_view simplifiedFileName(absl::string_view path, absl::string_view removedSuffix, absl::string_view ifEmpty); void updateSfzFileLabel(const std::string& filePath); void updateScalaFileLabel(const std::string& filePath); - static void updateLabelWithFileName(CTextLabel* label, const std::string& filePath); + static void updateLabelWithFileName(CTextLabel* label, const std::string& filePath, absl::string_view removedSuffix); + static void updateButtonWithFileName(CTextButton* button, const std::string& filePath, absl::string_view removedSuffix); void updateVolumeLabel(float volume); void updateNumVoicesLabel(int numVoices); void updateOversamplingLabel(int oversamplingLog2); @@ -142,7 +156,7 @@ void Editor::open(CFrame& frame) Impl& impl = *impl_; impl.frame_ = &frame; - frame.addView(impl.view_.get()); + frame.addView(impl.mainView_.get()); } void Editor::close() @@ -150,7 +164,7 @@ void Editor::close() Impl& impl = *impl_; if (impl.frame_) { - impl.frame_->removeView(impl.view_.get(), false); + impl.frame_->removeView(impl.mainView_.get(), false); impl.frame_ = nullptr; } } @@ -161,22 +175,27 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) case EditId::SfzFile: { const std::string& value = v.to_string(); + currentSfzFile_ = value; updateSfzFileLabel(value); } break; case EditId::Volume: { const float value = v.to_float(); - if (volumeSlider_) + if (volumeSlider_) { volumeSlider_->setValue(value); + volumeSlider_->setDirty(); + } updateVolumeLabel(value); } break; case EditId::Polyphony: { const int value = static_cast(v.to_float()); - if (numVoicesSlider_) + if (numVoicesSlider_) { numVoicesSlider_->setValue(value); + numVoicesSlider_->setDirty(); + } updateNumVoicesLabel(value); } break; @@ -188,16 +207,20 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) for (int f = value; f > 1; f /= 2) ++log2Value; - if (oversamplingSlider_) + if (oversamplingSlider_) { oversamplingSlider_->setValue(log2Value); + oversamplingSlider_->setDirty(); + } updateOversamplingLabel(log2Value); } break; case EditId::PreloadSize: { const int value = static_cast(v.to_float()); - if (preloadSizeSlider_) + if (preloadSizeSlider_) { preloadSizeSlider_->setValue(value); + preloadSizeSlider_->setDirty(); + } updatePreloadSizeLabel(value); } break; @@ -209,25 +232,35 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; case EditId::ScalaRootKey: { - const int value = static_cast(v.to_float()); - if (scalaRootKeySlider_) - scalaRootKeySlider_->setValue(value); + const int value = std::max(0, static_cast(v.to_float())); + if (scalaRootKeySlider_) { + scalaRootKeySlider_->setValue(value % 12); + scalaRootKeySlider_->setDirty(); + } + if (scalaRootOctaveSlider_) { + scalaRootOctaveSlider_->setValue(value / 12); + scalaRootOctaveSlider_->setDirty(); + } updateScalaRootKeyLabel(value); } break; case EditId::TuningFrequency: { const float value = v.to_float(); - if (tuningFrequencySlider_) + if (tuningFrequencySlider_) { tuningFrequencySlider_->setValue(value); + tuningFrequencySlider_->setDirty(); + } updateTuningFrequencyLabel(value); } break; case EditId::StretchTuning: { const float value = v.to_float(); - if (stretchedTuningSlider_) + if (stretchedTuningSlider_) { stretchedTuningSlider_->setValue(value); + stretchedTuningSlider_->setDirty(); + } updateStretchedTuningLabel(value); } break; @@ -284,405 +317,319 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) void Editor::Impl::createFrameContents() { - const CRect bounds { 0.0, 0.0, static_cast(viewWidth), static_cast(viewHeight) }; - CViewContainer* view = new CViewContainer(bounds); - view_ = owned(view); + CViewContainer* mainView; - view->setBackgroundColor(CColor(0xff, 0xff, 0xff)); + SharedPointer iconWhite = owned(new CBitmap("icon_white.png")); + SharedPointer knob48 = owned(new CBitmap("knob48.png")); + SharedPointer logoText = owned(new CBitmap("logo_text.png")); - SharedPointer logo = owned(new CBitmap("logo.png")); + { + const CColor frameBackground = { 0xd3, 0xd7, 0xcf }; - CRect bottomRow = bounds; - bottomRow.top = bottomRow.bottom - 30; + struct Theme { + CColor boxBackground; + CColor text; + CColor titleBoxText; + CColor titleBoxBackground; + CColor icon; + CColor valueText; + CColor valueBackground; + }; - CRect topRow = bounds; - topRow.bottom = topRow.top + 30; + Theme lightTheme; + lightTheme.boxBackground = { 0xba, 0xbd, 0xb6 }; + lightTheme.text = { 0x00, 0x00, 0x00 }; + lightTheme.titleBoxText = { 0xff, 0xff, 0xff }; + lightTheme.titleBoxBackground = { 0x2e, 0x34, 0x36 }; + lightTheme.icon = lightTheme.text; + lightTheme.valueText = { 0xff, 0xff, 0xff }; + lightTheme.valueBackground = { 0x2e, 0x34, 0x36 }; + Theme darkTheme; + darkTheme.boxBackground = { 0x2e, 0x34, 0x36 }; + darkTheme.text = { 0xff, 0xff, 0xff }; + darkTheme.titleBoxText = { 0x00, 0x00, 0x00 }; + darkTheme.titleBoxBackground = { 0xba, 0xbd, 0xb6 }; + darkTheme.icon = darkTheme.text; + darkTheme.valueText = { 0x2e, 0x34, 0x36 }; + darkTheme.valueBackground = { 0xff, 0xff, 0xff }; + Theme& defaultTheme = lightTheme; + Theme* theme = &defaultTheme; + auto enterTheme = [&theme](Theme& t) { theme = &t; }; + + typedef CViewContainer LogicalGroup; + typedef SBoxContainer RoundedGroup; + typedef STitleContainer TitleGroup; + typedef CKickButton SfizzMainButton; + typedef CTextLabel Label; + typedef CViewContainer HLine; + typedef CTextButton LightButton; + typedef CAnimKnob Knob48; + typedef CTextLabel ValueLabel; + typedef CViewContainer VMeter; + typedef CView SfizzLargePicture; + typedef SValueMenu ValueMenu; +#if 0 + typedef CTextButton Button; +#endif + typedef CTextButton ValueButton; + typedef CTextButton LoadFileButton; + typedef CTextButton EditFileButton; + typedef SPiano Piano; + + auto createLogicalGroup = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + CViewContainer* container = new CViewContainer(bounds); + container->setBackgroundColor(CColor(0x00, 0x00, 0x00, 0x00)); + return container; + }; + auto createRoundedGroup = [&theme](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + auto* box = new SBoxContainer(bounds); + box->setCornerRadius(10.0); + box->setBackgroundColor(theme->boxBackground); + return box; + }; + auto createTitleGroup = [&theme](const CRect& bounds, int, const char* label, CHoriTxtAlign, int fontsize) { + auto* box = new STitleContainer(bounds, label); + box->setCornerRadius(10.0); + box->setBackgroundColor(theme->boxBackground); + box->setTitleFontColor(theme->titleBoxText); + box->setTitleBackgroundColor(theme->titleBoxBackground); + auto font = owned(new CFontDesc(*box->getTitleFont())); + font->setSize(fontsize); + box->setTitleFont(font); + return box; + }; + auto createSfizzMainButton = [this, &iconWhite](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int) { + return new CKickButton(bounds, this, tag, iconWhite); + }; + auto createLabel = [&theme](const CRect& bounds, int, const char* label, CHoriTxtAlign align, int fontsize) { + CTextLabel* lbl = new CTextLabel(bounds, label); + lbl->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + lbl->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + lbl->setFontColor(theme->text); + lbl->setHoriAlign(align); + auto font = owned(new CFontDesc(*lbl->getFont())); + font->setSize(fontsize); + lbl->setFont(font); + return lbl; + }; + auto createHLine = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + int y = static_cast(0.5 * (bounds.top + bounds.bottom)); + CRect lineBounds(bounds.left, y, bounds.right, y + 1); + CViewContainer* hline = new CViewContainer(lineBounds); + hline->setBackgroundColor(CColor(0xff, 0xff, 0xff, 0xff)); + return hline; + }; + auto createLightButton = [this](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int) { + CTextButton* button = new CTextButton(bounds, this, tag, label); + button->setTextAlignment(align); + return button; + }; + auto createKnob48 = [this, &knob48](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int) { + return new CAnimKnob(bounds, this, tag, 31, 48, knob48); + }; + auto createValueLabel = [&theme](const CRect& bounds, int, const char* label, CHoriTxtAlign align, int fontsize) { + CTextLabel* lbl = new CTextLabel(bounds, label); + lbl->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + lbl->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + lbl->setFontColor(theme->text); + lbl->setHoriAlign(align); + auto font = owned(new CFontDesc(*lbl->getFont())); + font->setSize(fontsize); + lbl->setFont(font); + return lbl; + }; + auto createVMeter = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + // TODO the volume meter... + CViewContainer* container = new CViewContainer(bounds); + container->setBackgroundColor(CColor(0x00, 0x00, 0x00, 0x00)); + return container; + }; + auto createSfizzLargePicture = [&logoText](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + CView* picture = new CView(bounds); + picture->setBackground(logoText); + return picture; + }; +#if 0 + auto createButton = [this](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { + CTextButton* button = new CTextButton(bounds, this, tag, label); + auto font = owned(new CFontDesc(*button->getFont())); + font->setSize(fontsize); + button->setFont(font); + button->setTextAlignment(align); + return button; + }; +#endif + auto createValueButton = [this, &theme](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { + CTextButton* button = new CTextButton(bounds, this, tag, label); + auto font = owned(new CFontDesc(*button->getFont())); + font->setSize(fontsize); + button->setFont(font); + button->setTextAlignment(align); + button->setTextColor(theme->valueText); + button->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + SharedPointer gradient = owned(CGradient::create(0.0, 1.0, theme->valueBackground, theme->valueBackground)); + button->setGradient(gradient); + button->setGradientHighlighted(gradient); + return button; + }; + auto createValueMenu = [this, &theme](const CRect& bounds, int tag, const char*, CHoriTxtAlign align, int fontsize) { + SValueMenu* vm = new SValueMenu(bounds, this, tag); + vm->setHoriAlign(align); + auto font = owned(new CFontDesc(*vm->getFont())); + font->setSize(fontsize); + vm->setFont(font); + vm->setFontColor(theme->valueText); + vm->setBackColor(theme->valueBackground); + vm->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + vm->setStyle(CParamDisplay::kRoundRectStyle); + vm->setRoundRectRadius(5.0); + return vm; + }; + auto createGlyphButton = [this, &theme](UTF8StringPtr glyph, const CRect& bounds, int tag, int fontsize) { + CTextButton* btn = new CTextButton(bounds, this, tag, glyph); + btn->setFont(new CFontDesc("Fluent System Regular W20", fontsize)); + btn->setTextColor(theme->icon); + btn->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + btn->setGradient(nullptr); + btn->setGradientHighlighted(nullptr); + return btn; + }; + auto createLoadFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + return createGlyphButton("\ue142", bounds, tag, fontsize); + }; + auto createEditFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + return createGlyphButton("\ue148", bounds, tag, fontsize); + }; + auto createPiano = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + SPiano* piano = new SPiano(bounds); + return piano; + }; + + #include "layout/main.hpp" + + mainView->setBackgroundColor(frameBackground); + + mainView_ = owned(mainView); + } + + /// + SharedPointer fileDropTarget = owned(new SFileDropTarget); + + fileDropTarget->setFileDropFunction([this](const std::string& file) { + changeSfzFile(file); + }); + + mainView_->setDropTarget(fileDropTarget); + + /// + adjustMinMaxToEditRange(volumeSlider_, EditId::Volume); + adjustMinMaxToEditRange(numVoicesSlider_, EditId::Polyphony); + adjustMinMaxToEditRange(oversamplingSlider_, EditId::Oversampling); + adjustMinMaxToEditRange(preloadSizeSlider_, EditId::PreloadSize); + if (scalaRootKeySlider_) { + scalaRootKeySlider_->setMin(0.0); + scalaRootKeySlider_->setMax(11.0); + scalaRootKeySlider_->setDefaultValue( + static_cast(EditRange::get(EditId::ScalaRootKey).def) % 12); + } + if (scalaRootOctaveSlider_) { + scalaRootOctaveSlider_->setMin(0.0); + scalaRootOctaveSlider_->setMax(10.0); + scalaRootOctaveSlider_->setDefaultValue( + static_cast(EditRange::get(EditId::ScalaRootKey).def) / 12); + } + adjustMinMaxToEditRange(tuningFrequencySlider_, EditId::TuningFrequency); + adjustMinMaxToEditRange(stretchedTuningSlider_, EditId::StretchTuning); + + for (int value : {1, 2, 4, 8, 16, 32, 64, 96, 128, 160, 192, 224, 256}) + numVoicesSlider_->addEntry(std::to_string(value), value); + numVoicesSlider_->setValueToStringFunction2( + [](float value, std::string& result, CParamDisplay*) -> bool + { + result = std::to_string(static_cast(value)); + return true; + }); + + for (int log2value = 0; log2value <= 3; ++log2value) { + int value = 1 << log2value; + oversamplingSlider_->addEntry(std::to_string(value) + "x", log2value); + } + oversamplingSlider_->setValueToStringFunction2( + [](float value, std::string& result, CParamDisplay*) -> bool + { + result = std::to_string(1 << static_cast(value)) + "x"; + return true; + }); + + for (int log2value = 10; log2value <= 16; ++log2value) { + int value = 1 << log2value; + char text[256]; + sprintf(text, "%d kB", value / 1024); + text[sizeof(text) - 1] = '\0'; + preloadSizeSlider_->addEntry(text, value); + } + preloadSizeSlider_->setValueToStringFunction2( + [](float value, std::string& result, CParamDisplay*) -> bool + { + result = std::to_string(static_cast(std::round(value * (1.0 / 1024)))) + " kB"; + return true; + }); + + static const std::pair tuningFrequencies[] = { + {380.0f, "English pitchpipe 380 (1720)"}, + {409.0f, "Handel fork 409 (1780)"}, + {415.0f, "Baroque 415"}, + {422.5f, "Handel fork 422.5 (1740)"}, + {423.2f, "Dresden opera 423.2 (1815)"}, + {435.0f, "French Law 435 (1859)"}, + {439.0f, "British Phil 439 (1896)"}, + {440.0f, "International 440"}, + {442.0f, "European 442"}, + {445.0f, "Germany, China 445"}, + {451.0f, "La Scala in Milan 451 (18th)"}, + }; + + for (std::pair value : tuningFrequencies) + tuningFrequencySlider_->addEntry(value.second, value.first); + tuningFrequencySlider_->setValueToStringFunction( + [](float value, char result[256], CParamDisplay*) -> bool + { + sprintf(result, "%.1f Hz", value); + return true; + }); + + static const char* notesInOctave[12] = { + "C", "C#", "D", "D#", "E", + "F", "F#", "G", "G#", "A", "A#", "B", + }; + for (int note = 0; note < 12; ++note) + scalaRootKeySlider_->addEntry(notesInOctave[note], note); + for (int octave = 0; octave <= 10; ++octave) + scalaRootOctaveSlider_->addEntry(std::to_string(octave - 1), octave); + scalaRootKeySlider_->setValueToStringFunction2( + [](float value, std::string& result, CParamDisplay*) -> bool + { + result = notesInOctave[std::max(0, static_cast(value)) % 12]; + return true; + }); + scalaRootOctaveSlider_->setValueToStringFunction2( + [](float value, std::string& result, CParamDisplay*) -> bool + { + result = std::to_string(static_cast(value) - 1); + return true; + }); + + /// CViewContainer* panel; activePanel_ = 0; - CRect topLeftLabelBox = topRow; - topLeftLabelBox.right -= 20 * kNumPanels; - - // general panel - { - panel = new CViewContainer(bounds); - view->addView(panel); - panel->setTransparency(true); - - CKickButton* sfizzButton = new CKickButton(bounds, this, kTagLoadSfzFile, logo); - panel->addView(sfizzButton); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "No file loaded"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - sfzFileLabel_ = topLeftLabel; - - subPanels_[kPanelGeneral] = panel; - } - - // settings panel - { - panel = new CViewContainer(bounds); - view->addView(panel); - panel->setTransparency(true); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Settings"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - - CRect row = topRow; - row.top += 45.0; - row.bottom += 45.0; - row.left += 20.0; - row.right -= 20.0; - - static const CCoord interRow = 35.0; - static const CCoord interColumn = 20.0; - static const int numColumns = 3; - - auto nthColumn = [&row](int colIndex) -> CRect { - CRect div = row; - CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; - div.left = div.left + colIndex * (columnWidth + interColumn); - div.right = div.left + columnWidth; - return div; - }; - - CTextLabel* label; - SimpleSlider* slider; - - label = new CTextLabel(nthColumn(0), "Volume"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetVolume); - panel->addView(slider); - adjustMinMaxToEditRange(slider, EditId::Volume); - volumeSlider_ = slider; - label = new CTextLabel(nthColumn(2), ""); - volumeLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Polyphony"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetNumVoices); - panel->addView(slider); - adjustMinMaxToEditRange(slider, EditId::Polyphony); - numVoicesSlider_ = slider; - label = new CTextLabel(nthColumn(2), ""); - numVoicesLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Oversampling"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetOversampling); - panel->addView(slider); - adjustMinMaxToEditRange(slider, EditId::Oversampling); - oversamplingSlider_ = slider; - label = new CTextLabel(nthColumn(2), ""); - oversamplingLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Preload size"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetPreloadSize); - panel->addView(slider); - adjustMinMaxToEditRange(slider, EditId::PreloadSize); - preloadSizeSlider_ = slider; - label = new CTextLabel(nthColumn(2), ""); - preloadSizeLabel_ = label; - panel->addView(label); - - subPanels_[kPanelSettings] = panel; - } - - // tuning panel - { - panel = new CViewContainer(bounds); - view->addView(panel); - panel->setTransparency(true); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Tuning"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - - CRect row = topRow; - row.top += 45.0; - row.bottom += 45.0; - row.left += 20.0; - row.right -= 20.0; - - static const CCoord interRow = 35.0; - static const CCoord interColumn = 20.0; - static const int numColumns = 3; - - auto nthColumn = [&row](int colIndex) -> CRect { - CRect div = row; - CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; - div.left = div.left + colIndex * (columnWidth + interColumn); - div.right = div.left + columnWidth; - return div; - }; - - CTextLabel* label; - SimpleSlider* slider; - CTextButton* textbutton; - - label = new CTextLabel(nthColumn(0), "Scala file"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - textbutton = new CTextButton(nthColumn(1), this, kTagLoadScalaFile, "Choose"); - panel->addView(textbutton); - label = new CTextLabel(nthColumn(2), ""); - scalaFileLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Scala root key"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetScalaRootKey); - panel->addView(slider); - adjustMinMaxToEditRange(slider, EditId::ScalaRootKey); - scalaRootKeySlider_ = slider; - label = new CTextLabel(nthColumn(2), ""); - scalaRootKeyLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Tuning frequency"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetTuningFrequency); - panel->addView(slider); - adjustMinMaxToEditRange(slider, EditId::TuningFrequency); - tuningFrequencySlider_ = slider; - label = new CTextLabel(nthColumn(2), ""); - tuningFrequencyLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Stretched tuning"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - slider = new SimpleSlider(nthColumn(1), this, kTagSetStretchedTuning); - panel->addView(slider); - adjustMinMaxToEditRange(slider, EditId::StretchTuning); - stretchedTuningSlider_ = slider; - label = new CTextLabel(nthColumn(2), ""); - stretchedTuningLabel_ = label; - panel->addView(label); - - subPanels_[kPanelTuning] = panel; - } - - // info panel - { - panel = new CViewContainer(bounds); - view->addView(panel); - panel->setTransparency(true); - - CTextLabel* topLeftLabel = new CTextLabel(topLeftLabelBox, "Information"); - topLeftLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - topLeftLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(topLeftLabel); - - CRect row = topRow; - row.top += 45.0; - row.bottom += 45.0; - row.left += 20.0; - row.right -= 20.0; - - static const CCoord interRow = 20.0; - static const CCoord interColumn = 20.0; - static const int numColumns = 3; - - auto nthColumn = [&row](int colIndex) -> CRect { - CRect div = row; - CCoord columnWidth = (div.right - div.left + interColumn) / numColumns - interColumn; - div.left = div.left + colIndex * (columnWidth + interColumn); - div.right = div.left + columnWidth; - return div; - }; - - CTextLabel* label; - - label = new CTextLabel(nthColumn(0), "Curves"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - infoCurvesLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Masters"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - infoMastersLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Groups"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - infoGroupsLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Regions"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - infoRegionsLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Samples"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - infoSamplesLabel_ = label; - panel->addView(label); - - row.top += interRow; - row.bottom += interRow; - - label = new CTextLabel(nthColumn(0), "Voices"); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - panel->addView(label); - label = new CTextLabel(nthColumn(1), ""); - label->setFontColor(CColor(0x00, 0x00, 0x00)); - label->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - label->setHoriAlign(kLeftText); - infoVoicesLabel_ = label; - panel->addView(label); - - subPanels_[kPanelInfo] = panel; - } - // all panels for (unsigned currentPanel = 0; currentPanel < kNumPanels; ++currentPanel) { panel = subPanels_[currentPanel]; - CTextLabel* descLabel = new CTextLabel( - bottomRow, "Paul Ferrand and the SFZ Tools work group"); - descLabel->setFontColor(CColor(0x00, 0x00, 0x00)); - descLabel->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); - panel->addView(descLabel); - - for (unsigned i = 0; i < kNumPanels; ++i) { - CRect btnRect = topRow; - btnRect.left = topRow.right - (kNumPanels - i) * 50; - btnRect.right = btnRect.left + 50; - - const char *text; - switch (i) { - case kPanelGeneral: text = "File"; break; - case kPanelSettings: text = "Setup"; break; - case kPanelTuning: text = "Tuning"; break; - case kPanelInfo: text = "Info"; break; - default: text = "?"; break; - } - - CTextButton* changePanelButton = new CTextButton(btnRect, this, kTagFirstChangePanel + i, text); - panel->addView(changePanelButton); - - changePanelButton->setRoundRadius(0.0); - } + if (!panel) + continue; panel->setVisible(currentPanel == activePanel_); } @@ -697,14 +644,18 @@ void Editor::Impl::chooseSfzFile() if (fs->runModal()) { UTF8StringPtr file = fs->getSelectedFile(0); - if (file) { - std::string str(file); - ctrl_->uiSendValue(EditId::SfzFile, str); - updateSfzFileLabel(str); - } + if (file) + changeSfzFile(file); } } +void Editor::Impl::changeSfzFile(const std::string& filePath) +{ + ctrl_->uiSendValue(EditId::SfzFile, filePath); + currentSfzFile_ = filePath; + updateSfzFileLabel(filePath); +} + void Editor::Impl::chooseScalaFile() { SharedPointer fs = owned(CNewFileSelector::create(frame_)); @@ -714,42 +665,64 @@ void Editor::Impl::chooseScalaFile() if (fs->runModal()) { UTF8StringPtr file = fs->getSelectedFile(0); - if (file) { - std::string str(file); - ctrl_->uiSendValue(EditId::ScalaFile, str); - updateScalaFileLabel(str); - } + if (file) + changeScalaFile(file); } } +void Editor::Impl::changeScalaFile(const std::string& filePath) +{ + ctrl_->uiSendValue(EditId::ScalaFile, filePath); + updateScalaFileLabel(filePath); +} + +absl::string_view Editor::Impl::simplifiedFileName(absl::string_view path, absl::string_view removedSuffix, absl::string_view ifEmpty) +{ + if (path.empty()) + return ifEmpty; + +#if defined (_WIN32) + size_t pos = path.find_last_of("/\\"); +#else + size_t pos = path.rfind('/'); +#endif + path = (pos != path.npos) ? path.substr(pos + 1) : path; + + if (!removedSuffix.empty() && absl::EndsWithIgnoreCase(path, removedSuffix)) + path.remove_suffix(removedSuffix.size()); + + return path; +} + void Editor::Impl::updateSfzFileLabel(const std::string& filePath) { - updateLabelWithFileName(sfzFileLabel_, filePath); + updateLabelWithFileName(sfzFileLabel_, filePath, ".sfz"); } void Editor::Impl::updateScalaFileLabel(const std::string& filePath) { - updateLabelWithFileName(scalaFileLabel_, filePath); + updateLabelWithFileName(scalaFileLabel_, filePath, ".scl"); + updateButtonWithFileName(scalaFileButton_, filePath, ".scl"); } -void Editor::Impl::updateLabelWithFileName(CTextLabel* label, const std::string& filePath) +void Editor::Impl::updateLabelWithFileName(CTextLabel* label, const std::string& filePath, absl::string_view removedSuffix) { if (!label) return; - std::string fileName; - if (filePath.empty()) - fileName = ""; - else { -#if defined (_WIN32) - size_t pos = filePath.find_last_of("/\\"); -#else - size_t pos = filePath.rfind('/'); -#endif - fileName = (pos != filePath.npos) ? - filePath.substr(pos + 1) : filePath; - } + std::string fileName = std::string(simplifiedFileName(filePath, removedSuffix, "")); label->setText(fileName.c_str()); + label->setDirty(); +} + +void Editor::Impl::updateButtonWithFileName(CTextButton* button, const std::string& filePath, absl::string_view removedSuffix) +{ + if (!button) + return; + + std::string fileName = std::string(simplifiedFileName(filePath, removedSuffix, "")); + button->setTitle(fileName.c_str()); + button->setDirty(); } void Editor::Impl::updateVolumeLabel(float volume) @@ -762,6 +735,7 @@ void Editor::Impl::updateVolumeLabel(float volume) sprintf(text, "%.1f dB", volume); text[sizeof(text) - 1] = '\0'; label->setText(text); + label->setDirty(); } void Editor::Impl::updateNumVoicesLabel(int numVoices) @@ -774,6 +748,7 @@ void Editor::Impl::updateNumVoicesLabel(int numVoices) sprintf(text, "%d", numVoices); text[sizeof(text) - 1] = '\0'; label->setText(text); + label->setDirty(); } void Editor::Impl::updateOversamplingLabel(int oversamplingLog2) @@ -786,6 +761,7 @@ void Editor::Impl::updateOversamplingLabel(int oversamplingLog2) sprintf(text, "%dx", 1 << oversamplingLog2); text[sizeof(text) - 1] = '\0'; label->setText(text); + label->setDirty(); } void Editor::Impl::updatePreloadSizeLabel(int preloadSize) @@ -795,9 +771,10 @@ void Editor::Impl::updatePreloadSizeLabel(int preloadSize) return; char text[64]; - sprintf(text, "%.1f kB", preloadSize * (1.0 / 1024)); + sprintf(text, "%d kB", static_cast(std::round(preloadSize * (1.0 / 1024)))); text[sizeof(text) - 1] = '\0'; label->setText(text); + label->setDirty(); } void Editor::Impl::updateScalaRootKeyLabel(int rootKey) @@ -826,6 +803,7 @@ void Editor::Impl::updateScalaRootKeyLabel(int rootKey) }; label->setText(noteName(rootKey)); + label->setDirty(); } void Editor::Impl::updateTuningFrequencyLabel(float tuningFrequency) @@ -838,6 +816,7 @@ void Editor::Impl::updateTuningFrequencyLabel(float tuningFrequency) sprintf(text, "%.1f", tuningFrequency); text[sizeof(text) - 1] = '\0'; label->setText(text); + label->setDirty(); } void Editor::Impl::updateStretchedTuningLabel(float stretchedTuning) @@ -850,6 +829,7 @@ void Editor::Impl::updateStretchedTuningLabel(float stretchedTuning) sprintf(text, "%.3f", stretchedTuning); text[sizeof(text) - 1] = '\0'; label->setText(text); + label->setDirty(); } void Editor::Impl::setActivePanel(unsigned panelId) @@ -857,9 +837,11 @@ void Editor::Impl::setActivePanel(unsigned panelId) panelId = std::max(0, std::min(kNumPanels - 1, static_cast(panelId))); if (activePanel_ != panelId) { - subPanels_[activePanel_]->setVisible(false); + if (subPanels_[activePanel_]) + subPanels_[activePanel_]->setVisible(false); + if (subPanels_[panelId]) + subPanels_[panelId]->setVisible(true); activePanel_ = panelId; - subPanels_[panelId]->setVisible(true); } } @@ -893,6 +875,14 @@ void Editor::Impl::valueChanged(CControl* ctl) Call::later([this]() { chooseSfzFile(); }); break; + case kTagEditSfzFile: + if (value != 1) + break; + + if (!currentSfzFile_.empty()) + openFileInExternalEditor(currentSfzFile_.c_str()); + break; + case kTagLoadScalaFile: if (value != 1) break; @@ -921,8 +911,15 @@ void Editor::Impl::valueChanged(CControl* ctl) break; case kTagSetScalaRootKey: - ctrl.uiSendValue(EditId::ScalaRootKey, value); - updateScalaRootKeyLabel(static_cast(value)); + { + if (scalaRootKeySlider_ && scalaRootOctaveSlider_) { + int key = static_cast(scalaRootKeySlider_->getValue()); + int octave = static_cast(scalaRootOctaveSlider_->getValue()); + int midiKey = key + 12 * octave; + ctrl.uiSendValue(EditId::ScalaRootKey, midiKey); + updateScalaRootKeyLabel(midiKey); + } + } break; case kTagSetTuningFrequency: diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index a85bf64b..c4e6f958 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -5,34 +5,422 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "GUIComponents.h" +#include #include "utility/vstgui_before.h" #include "vstgui/lib/cdrawcontext.h" +#include "vstgui/lib/cgraphicspath.h" +#include "vstgui/lib/cframe.h" #include "utility/vstgui_after.h" -SimpleSlider::SimpleSlider(const CRect& bounds, IControlListener* listener, int32_t tag) - : CSliderBase(bounds, listener, tag) +/// +SBoxContainer::SBoxContainer(const CRect& size) + : CViewContainer(size) { - setStyle(kHorizontal|kLeft); - - CPoint offsetHandle(2.0, 2.0); - setOffsetHandle(offsetHandle); - - CCoord handleSize = 20.0; - setHandleSizePrivate(handleSize, bounds.bottom - bounds.top - 2 * offsetHandle.y); - setHandleRangePrivate(bounds.right - bounds.left - handleSize - 2 * offsetHandle.x); + CViewContainer::setBackgroundColor(CColor(0, 0, 0, 0)); } -void SimpleSlider::draw(CDrawContext* dc) +void SBoxContainer::setCornerRadius(CCoord radius) +{ + cornerRadius_ = radius; + setDirty(); +} + +void SBoxContainer::setBackgroundColor(const CColor& color) +{ + backgroundColor_ = color; + setDirty(); +} + +CColor SBoxContainer::getBackgroundColor() const +{ + return backgroundColor_; +} + +void SBoxContainer::drawRect(CDrawContext* dc, const CRect& updateRect) { CRect bounds = getViewSize(); - CRect handle = calculateHandleRect(getValueNormalized()); dc->setDrawMode(kAntiAliasing); - dc->setFrameColor(_frame); - dc->drawRect(bounds, kDrawStroked); + SharedPointer path = owned(dc->createGraphicsPath()); + path->addRoundRect(bounds, cornerRadius_); - dc->setFillColor(_fill); - dc->drawRect(handle, kDrawFilled); + dc->setFillColor(backgroundColor_); + dc->drawGraphicsPath(path.get(), CDrawContext::kPathFilled); + + CViewContainer::drawRect(dc, updateRect); +} + +/// +STitleContainer::STitleContainer(const CRect& size, UTF8StringPtr text) + : SBoxContainer(size), text_(text ? text : ""), titleFont_(kNormalFont) +{ +} + +void STitleContainer::setTitleFont(CFontRef font) +{ + titleFont_ = font; + setDirty(); +} + +void STitleContainer::setTitleFontColor(CColor color) +{ + titleFontColor_ = color; + setDirty(); +} + +void STitleContainer::setTitleBackgroundColor(CColor color) +{ + titleBackgroundColor_ = color; + setDirty(); +} + +void STitleContainer::drawRect(CDrawContext* dc, const CRect& updateRect) +{ + SBoxContainer::drawRect(dc, updateRect); + + CRect bounds = getViewSize(); + CCoord cornerRadius = cornerRadius_; + + dc->setDrawMode(kAntiAliasing); + + CCoord fontHeight = titleFont_->getSize(); + CCoord titleHeight = fontHeight + 8.0; + + CRect titleBounds = bounds; + titleBounds.bottom = titleBounds.top + titleHeight; + + SharedPointer path = owned(dc->createGraphicsPath()); + path->beginSubpath(titleBounds.getBottomRight()); + path->addLine(titleBounds.getBottomLeft()); + path->addArc(CRect(titleBounds.left, titleBounds.top, titleBounds.left + 2.0 * cornerRadius, titleBounds.top + 2.0 * cornerRadius), 180., 270., true); + path->addArc(CRect(titleBounds.right - 2.0 * cornerRadius, titleBounds.top, titleBounds.right, titleBounds.top + 2.0 * cornerRadius), 270., 360., true); + path->closeSubpath(); + + dc->setFillColor(titleBackgroundColor_); + dc->drawGraphicsPath(path, CDrawContext::kPathFilled); + + dc->setFont(titleFont_); + dc->setFontColor(titleFontColor_); + dc->drawString(text_.c_str(), titleBounds, kCenterText); +} + +/// +void SFileDropTarget::setFileDropFunction(FileDropFunction f) +{ + dropFunction_ = std::move(f); +} + +DragOperation SFileDropTarget::onDragEnter(DragEventData data) +{ + op_ = isFileDrop(data.drag) ? + DragOperation::Copy : DragOperation::None; + return op_; +} + +DragOperation SFileDropTarget::onDragMove(DragEventData data) +{ + (void)data; + return op_; +} + +void SFileDropTarget::onDragLeave(DragEventData data) +{ + (void)data; + op_ = DragOperation::None; +} + +bool SFileDropTarget::onDrop(DragEventData data) +{ + if (op_ != DragOperation::Copy || !isFileDrop(data.drag)) + return false; + + IDataPackage::Type type; + const void* bytes; + uint32_t size = data.drag->getData(0, bytes, type); + std::string path(reinterpret_cast(bytes), size); + + if (dropFunction_) + dropFunction_(path); + + return true; +} + +bool SFileDropTarget::isFileDrop(IDataPackage* package) +{ + return package->getCount() == 1 && + package->getDataType(0) == IDataPackage::kFilePath; +} + +/// +SPiano::SPiano(const CRect& bounds) + : CView(bounds), font_(kNormalFont) +{ +} + +void SPiano::setFont(CFontRef font) +{ + font_ = font; + setDirty(); +} + +void SPiano::clearKeyRanges() +{ + keyInRange_.reset(); +} + +void SPiano::addKeyRange(int start, int end) +{ + start = std::min(127, std::max(0, start)); + end = std::min(127, std::max(0, end)); + + for (int x = start; x <= end; ++x) + keyInRange_.set(x); +} + +CCoord SPiano::getKeyWidth() +{ + return 6.0; +} + +CCoord SPiano::getKeySwitchesHeight() +{ + return 20.0; +} + +CCoord SPiano::getKeyRangesHeight() +{ + return 11.0; +} + +CCoord SPiano::getKeysHeight() const +{ + return getHeight() - + (getKeySwitchesHeight() + getKeyRangesHeight() + getOctavesHeight()); +} + +CCoord SPiano::getOctavesHeight() const +{ + return font_->getSize(); +} + +void SPiano::getZoneDimensions( + CRect* pKeySwitches, + CRect* pKeyboard, + CRect* pKeyRanges, + CRect* pOctaves) +{ + CRect bounds = getViewSize(); + + CRect keySwitches(bounds); + keySwitches.setHeight(getKeySwitchesHeight()); + + CRect keyboard(bounds); + keyboard.top = keySwitches.bottom; + keyboard.setHeight(getKeysHeight()); + + CRect keyRanges(bounds); + keyRanges.top = keyboard.bottom; + keyRanges.setHeight(getKeyRangesHeight()); + + CRect octaves(bounds); + octaves.top = keyRanges.bottom; + octaves.setHeight(getOctavesHeight()); + + // apply some paddings + keySwitches.extend(-2.0, -2.0); + keyboard.extend(-2.0, -2.0); + keyRanges.extend(-2.0, -4.0); + octaves.extend(-2.0, -2.0); + + // offsets for centered keyboard + CCoord keyWidth = getKeyWidth(); + CCoord offset = std::round((keyboard.getWidth() - (128.0 * keyWidth)) * 0.5); + if (offset > 0) { + keySwitches.extend(-offset, 0.0); + keyboard.extend(-offset, 0.0); + keyRanges.extend(-offset, 0.0); + octaves.extend(-offset, 0.0); + } + + // + if (pKeySwitches) + *pKeySwitches = keySwitches; + if (pKeyboard) + *pKeyboard = keyboard; + if (pKeyRanges) + *pKeyRanges = keyRanges; + if (pOctaves) + *pOctaves = octaves; +} + +void SPiano::draw(CDrawContext* dc) +{ + CRect bounds = getViewSize(); + + dc->setDrawMode(kAntiAliasing); + + SharedPointer path; + + path = owned(dc->createGraphicsPath()); + path->addRoundRect(bounds, 5.0); + dc->setFillColor(CColor(0xca, 0xca, 0xca)); + dc->drawGraphicsPath(path, CDrawContext::kPathFilled); + + // + CRect rectKeySwitches; + CRect rectKeyboard; + CRect rectKeyRanges; + CRect rectOctaves; + getZoneDimensions(&rectKeySwitches, &rectKeyboard, &rectKeyRanges, &rectOctaves); + + // + path = owned(dc->createGraphicsPath()); + path->addRoundRect(rectKeyboard, 1.0); + dc->setFillColor(CColor(0xff, 0xff, 0xff)); + dc->drawGraphicsPath(path, CDrawContext::kPathFilled); + + CCoord keyWidth = getKeyWidth(); + for (int key = 0; key < 128; ++key) { + CCoord keyX = rectKeyboard.left + key * keyWidth; + int key12 = key % 12; + if (key12 == 1 || key12 == 3 || + key12 == 6 || key12 == 8 || key12 == 10) + { + CRect blackRect(keyX, rectKeyboard.top + 2, keyX + keyWidth, rectKeyboard.bottom - 2); + path = owned(dc->createGraphicsPath()); + path->addRoundRect(blackRect, 1.0); + dc->setFillColor(CColor(0x02, 0x02, 0x02)); + dc->drawGraphicsPath(path, CDrawContext::kPathFilled); + } + if (key != 0 && key12 == 0) { + dc->setLineWidth(1.5); + dc->setFrameColor(CColor(0x63, 0x63, 0x63)); + dc->drawLine(CPoint(keyX, rectKeyboard.top), CPoint(keyX, rectKeyboard.bottom)); + } + if (key12 == 5) { + CCoord pad = rectKeyboard.getHeight() * 0.4; + dc->setLineWidth(1.0); + dc->setFrameColor(CColor(0x63, 0x63, 0x63)); + dc->drawLine(CPoint(keyX, rectKeyboard.top + pad), CPoint(keyX, rectKeyboard.bottom - pad)); + } + } + + // + + for (int rangeStart = 0; rangeStart < 128;) + { + if (!keyInRange_[rangeStart]) { + ++rangeStart; + } + else { + int rangeEnd = rangeStart; + while (rangeEnd + 1 < 128 && keyInRange_[rangeEnd + 1]) + ++rangeEnd; + + CCoord rangeStartX = rectKeyRanges.left + rangeStart * keyWidth; + CCoord rangeEndX = rectKeyRanges.left + (rangeEnd + 1.0) * keyWidth; + CRect rectRange(rangeStartX, rectKeyRanges.top, rangeEndX, rectKeyRanges.bottom); + + path = owned(dc->createGraphicsPath()); + path->addRoundRect(rectRange, 2.0); + dc->setFillColor(CColor(0x0f, 0x0f, 0x0f)); + dc->drawGraphicsPath(path, CDrawContext::kPathFilled); + + rangeStart = rangeEnd + 1; + } + } + + // + + for (int key = 0; key < 128; ++key) { + CCoord keyX = rectOctaves.left + key * keyWidth; + int key12 = key % 12; + if (key12 == 0) { + CRect textRect(keyX, rectOctaves.top, keyX + 12 * keyWidth, rectOctaves.bottom); + dc->setFont(font_); + dc->setFontColor(CColor(0x63, 0x63, 0x63)); + dc->drawString(std::to_string(key / 12 - 1).c_str(), textRect, kLeftText); + } + } + + // +} + +/// +SValueMenu::SValueMenu(const CRect& bounds, IControlListener* listener, int32_t tag) + : CParamDisplay(bounds), menuListener_(owned(new MenuListener(*this))) +{ + setListener(listener); + setTag(tag); +} + +CMenuItem* SValueMenu::addEntry(CMenuItem* item, float value, int32_t index) +{ + if (index < 0 || index > getNbEntries()) { + menuItems_.emplace_back(owned(item)); + menuItemValues_.emplace_back(value); + } + else + { + menuItems_.insert(menuItems_.begin() + index, owned(item)); + menuItemValues_.insert(menuItemValues_.begin() + index, value); + } + return item; +} + +CMenuItem* SValueMenu::addEntry(const UTF8String& title, float value, int32_t index, int32_t itemFlags) +{ + if (title == "-") + return addSeparator(index); + CMenuItem* item = new CMenuItem(title, nullptr, 0, nullptr, itemFlags); + return addEntry(item, value, index); +} + +CMenuItem* SValueMenu::addSeparator(int32_t index) +{ + CMenuItem* item = new CMenuItem("", nullptr, 0, nullptr, CMenuItem::kSeparator); + return addEntry(item, 0.0f, index); +} + +int32_t SValueMenu::getNbEntries() const +{ + return static_cast(menuItems_.size()); +} + +CMouseEventResult SValueMenu::onMouseDown(CPoint& where, const CButtonState& buttons) +{ + (void)where; + + if (buttons & (kLButton|kRButton|kApple)) { + CFrame* frame = getFrame(); + CRect bounds = getViewSize(); + + CPoint frameWhere = bounds.getBottomLeft(); + this->localToFrame(frameWhere); + + auto self = shared(this); + frame->doAfterEventProcessing([self, frameWhere]() { + if (CFrame* frame = self->getFrame()) { + SharedPointer menu = owned(new COptionMenu(CRect(), self->menuListener_, -1, nullptr, nullptr, COptionMenu::kPopupStyle)); + for (const SharedPointer& item : self->menuItems_) { + menu->addEntry(item); + item->remember(); // above call does not increment refcount + } + menu->popup(frame, frameWhere + CPoint(0.0, 1.0)); + } + }); + return kMouseDownEventHandledButDontNeedMovedOrUpEvents; + } + + return kMouseEventNotHandled; +} + +void SValueMenu::onItemClicked(int32_t index) +{ + float oldValue = getValue(); + setValue(menuItemValues_[index]); + if (getValue() != oldValue) + valueChanged(); } diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index c76c2e44..8c295f5e 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -5,22 +5,144 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include #include "utility/vstgui_before.h" #include "vstgui/lib/controls/cslider.h" +#include "vstgui/lib/controls/cknob.h" +#include "vstgui/lib/controls/ctextlabel.h" +#include "vstgui/lib/controls/coptionmenu.h" +#include "vstgui/lib/cviewcontainer.h" #include "vstgui/lib/ccolor.h" +#include "vstgui/lib/dragging.h" #include "utility/vstgui_after.h" using namespace VSTGUI; -class SimpleSlider : public CSliderBase { +/// +class SBoxContainer : public CViewContainer { public: - SimpleSlider(const CRect& bounds, IControlListener* listener, int32_t tag); - void draw(CDrawContext* dc) override; + explicit SBoxContainer(const CRect& size); + virtual ~SBoxContainer() {} + void setCornerRadius(CCoord radius); + void setBackgroundColor(const CColor& color) override; + CColor getBackgroundColor() const override; - CLASS_METHODS(SimpleSlider, CSliderBase) +protected: + void drawRect(CDrawContext* dc, const CRect& updateRect) override; + +protected: + CCoord cornerRadius_ = 0.0; + CColor backgroundColor_; +}; + +/// +class STitleContainer : public SBoxContainer { +public: + explicit STitleContainer(const CRect& size, UTF8StringPtr text = nullptr); + ~STitleContainer() {} + + void setTitleFont(CFontRef font); + CFontRef getTitleFont() { return titleFont_; } + + void setTitleFontColor(CColor color); + CColor getTitleFontColor() const { return titleFontColor_; } + void setTitleBackgroundColor(CColor color); + CColor getTitleBackgroundColor() const { return titleBackgroundColor_; } + +protected: + void drawRect(CDrawContext* dc, const CRect& updateRect) override; private: - CColor _frame = CColor(0x00, 0x00, 0x00); - CColor _fill = CColor(0x00, 0x00, 0x00); + std::string text_; + CColor titleFontColor_; + CColor titleBackgroundColor_; + SharedPointer titleFont_; +}; + +/// +class SFileDropTarget : public IDropTarget, + public NonAtomicReferenceCounted { +public: + typedef std::function FileDropFunction; + void setFileDropFunction(FileDropFunction f); + +protected: + DragOperation onDragEnter(DragEventData data) override; + DragOperation onDragMove(DragEventData data) override; + void onDragLeave(DragEventData data) override; + bool onDrop(DragEventData data) override; + +private: + static bool isFileDrop(IDataPackage* package); + +private: + DragOperation op_ = DragOperation::None; + FileDropFunction dropFunction_; +}; + +/// +class SPiano : public CView { +public: + explicit SPiano(const CRect& bounds); + CFontRef getFont() const { return font_; } + void setFont(CFontRef font); + + void clearKeyRanges(); + void addKeyRange(int start, int end); + +protected: + static CCoord getKeyWidth(); + static CCoord getKeySwitchesHeight(); + static CCoord getKeyRangesHeight(); + CCoord getKeysHeight() const; + CCoord getOctavesHeight() const; + + void getZoneDimensions( + CRect* pKeySwitches, + CRect* pKeyboard, + CRect* pKeyRanges, + CRect* pOctaves); + + void draw(CDrawContext* dc) override; + +private: + SharedPointer font_; + std::bitset<128> keyInRange_; +}; + +/// +class SValueMenu : public CParamDisplay { +public: + explicit SValueMenu(const CRect& bounds, IControlListener* listener, int32_t tag); + CMenuItem* addEntry(CMenuItem* item, float value, int32_t index = -1); + CMenuItem* addEntry(const UTF8String& title, float value, int32_t index = -1, int32_t itemFlags = CMenuItem::kNoFlags); + CMenuItem* addSeparator(int32_t index = -1); + int32_t getNbEntries() const; + +protected: + CMouseEventResult onMouseDown(CPoint& where, const CButtonState& buttons); + +private: + class MenuListener; + + // + void onItemClicked(int32_t index); + + // + CMenuItemList menuItems_; + std::vector menuItemValues_; + SharedPointer menuListener_; + + // + class MenuListener : public IControlListener, public NonAtomicReferenceCounted { + public: + explicit MenuListener(SValueMenu& menu) : menu_(menu) {} + void valueChanged(CControl* control) override + { + menu_.onItemClicked(static_cast(control->getValue())); + } + private: + SValueMenu& menu_; + }; }; diff --git a/editor/src/editor/NativeHelpers.cpp b/editor/src/editor/NativeHelpers.cpp new file mode 100644 index 00000000..ab760d73 --- /dev/null +++ b/editor/src/editor/NativeHelpers.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "NativeHelpers.h" + +#if defined(_WIN32) +#include "ghc/fs_std.hpp" +#include +#include + +bool openFileInExternalEditor(const char *filename) +{ + std::wstring path = fs::u8path(filename).wstring(); + + SHELLEXECUTEINFOW info; + memset(&info, 0, sizeof(info)); + + info.cbSize = sizeof(info); + info.fMask = SEE_MASK_CLASSNAME; + info.lpVerb = L"open"; + info.lpFile = path.c_str(); + info.lpClass = L"txtfile"; + info.nShow = SW_SHOW; + + return ShellExecuteExW(&info); +} +#elif defined(__APPLE__) + // implemented in NativeHelpers.mm +#else +#include + +bool openFileInExternalEditor(const char *filename) +{ + GAppInfo* appinfo = g_app_info_get_default_for_type("text/plain", FALSE); + if (!appinfo) + return 1; + + GList* files = nullptr; + GFile* file = g_file_new_for_path(filename); + files = g_list_append(files, file); + gboolean success = g_app_info_launch(appinfo, files, nullptr, nullptr); + g_object_unref(file); + g_list_free(files); + g_object_unref(appinfo); + return success == TRUE; +} +#endif diff --git a/editor/src/editor/NativeHelpers.h b/editor/src/editor/NativeHelpers.h new file mode 100644 index 00000000..b9c29bb6 --- /dev/null +++ b/editor/src/editor/NativeHelpers.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once + +bool openFileInExternalEditor(const char *filename); diff --git a/editor/src/editor/NativeHelpers.mm b/editor/src/editor/NativeHelpers.mm new file mode 100644 index 00000000..9cb8cd28 --- /dev/null +++ b/editor/src/editor/NativeHelpers.mm @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "NativeHelpers.h" + +#if defined(__APPLE__) +#import +#import +#import + +bool openFileInExternalEditor(const char *fileNameUTF8) +{ + BOOL wasOpened = NO; + + NSURL* applicationURL = (__bridge_transfer NSURL*)LSCopyDefaultApplicationURLForContentType( + kUTTypePlainText, kLSRolesEditor, nil); + if (!applicationURL) + return false; + if ([applicationURL isFileURL]) { + NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; + NSString* fileName = [NSString stringWithUTF8String:fileNameUTF8]; + wasOpened = [workspace openFile:fileName withApplication:[applicationURL path]]; + } + + return wasOpened == YES; +} +#endif diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp new file mode 100644 index 00000000..808e5a10 --- /dev/null +++ b/editor/src/editor/layout/main.hpp @@ -0,0 +1,167 @@ +/* This file is generated by the layout maker tool. */ +LogicalGroup* const view__0 = createLogicalGroup(CRect(0, 0, 800, 475), -1, "", kCenterText, 14); +mainView = view__0; +enterTheme(darkTheme); +LogicalGroup* const view__1 = createLogicalGroup(CRect(0, 0, 800, 110), -1, "", kCenterText, 14); +view__0->addView(view__1); +RoundedGroup* const view__2 = createRoundedGroup(CRect(5, 4, 105, 105), -1, "", kCenterText, 14); +view__1->addView(view__2); +SfizzMainButton* const view__3 = createSfizzMainButton(CRect(2, 2, 98, 98), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 14); +view__2->addView(view__3); +RoundedGroup* const view__4 = createRoundedGroup(CRect(110, 5, 490, 105), -1, "", kCenterText, 14); +view__1->addView(view__4); +Label* const view__5 = createLabel(CRect(15, 10, 55, 35), -1, "File:", kCenterText, 16); +view__4->addView(view__5); +Label* const view__6 = createLabel(CRect(15, 40, 55, 65), -1, "KS:", kCenterText, 16); +view__4->addView(view__6); +HLine* const view__7 = createHLine(CRect(10, 35, 365, 40), -1, "", kCenterText, 14); +view__4->addView(view__7); +HLine* const view__8 = createHLine(CRect(10, 65, 365, 70), -1, "", kCenterText, 14); +view__4->addView(view__8); +Label* const view__9 = createLabel(CRect(80, 10, 310, 35), -1, "DefaultInstrument.sfz", kCenterText, 20); +sfzFileLabel_ = view__9; +view__4->addView(view__9); +Label* const view__10 = createLabel(CRect(80, 40, 310, 65), -1, "Key switch", kCenterText, 20); +view__4->addView(view__10); +Label* const view__11 = createLabel(CRect(10, 70, 70, 95), -1, "Voices:", kRightText, 12); +view__4->addView(view__11); +LoadFileButton* const view__12 = createLoadFileButton(CRect(315, 10, 340, 35), kTagLoadSfzFile, "", kCenterText, 24); +view__4->addView(view__12); +EditFileButton* const view__13 = createEditFileButton(CRect(340, 10, 365, 35), kTagEditSfzFile, "", kCenterText, 24); +view__4->addView(view__13); +Label* const view__14 = createLabel(CRect(75, 70, 125, 95), -1, "", kCenterText, 12); +infoVoicesLabel_ = view__14; +view__4->addView(view__14); +Label* const view__15 = createLabel(CRect(130, 70, 190, 95), -1, "Max:", kRightText, 12); +view__4->addView(view__15); +Label* const view__16 = createLabel(CRect(195, 70, 245, 95), -1, "", kCenterText, 12); +numVoicesLabel_ = view__16; +view__4->addView(view__16); +Label* const view__17 = createLabel(CRect(250, 70, 310, 95), -1, "Memory:", kRightText, 12); +view__4->addView(view__17); +Label* const view__18 = createLabel(CRect(315, 70, 365, 95), -1, "", kCenterText, 12); +memoryLabel_ = view__18; +view__4->addView(view__18); +RoundedGroup* const view__19 = createRoundedGroup(CRect(495, 5, 595, 105), -1, "", kCenterText, 14); +view__1->addView(view__19); +LightButton* const view__20 = createLightButton(CRect(15, 37, 85, 62), kTagFirstChangePanel+kPanelSettings, "SETUP", kCenterText, 14); +view__19->addView(view__20); +LightButton* const view__21 = createLightButton(CRect(15, 10, 85, 35), kTagFirstChangePanel+kPanelControls, "CC", kCenterText, 14); +view__19->addView(view__21); +LightButton* const view__22 = createLightButton(CRect(15, 64, 85, 89), kTagFirstChangePanel+kPanelInfo, "INFO", kCenterText, 14); +view__19->addView(view__22); +RoundedGroup* const view__23 = createRoundedGroup(CRect(600, 5, 795, 105), -1, "", kCenterText, 14); +view__1->addView(view__23); +Knob48* const view__24 = createKnob48(CRect(15, 15, 63, 63), -1, "", kCenterText, 14); +view__23->addView(view__24); +view__24->setVisible(false); +ValueLabel* const view__25 = createValueLabel(CRect(10, 65, 70, 90), -1, "Center", kCenterText, 12); +view__23->addView(view__25); +view__25->setVisible(false); +Knob48* const view__26 = createKnob48(CRect(80, 15, 128, 63), kTagSetVolume, "", kCenterText, 14); +volumeSlider_ = view__26; +view__23->addView(view__26); +ValueLabel* const view__27 = createValueLabel(CRect(75, 65, 135, 90), -1, "0.0 dB", kCenterText, 12); +volumeLabel_ = view__27; +view__23->addView(view__27); +VMeter* const view__28 = createVMeter(CRect(145, 15, 180, 85), -1, "", kCenterText, 14); +view__23->addView(view__28); +enterTheme(defaultTheme); +LogicalGroup* const view__29 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelGeneral] = view__29; +view__0->addView(view__29); +view__29->setVisible(false); +RoundedGroup* const view__30 = createRoundedGroup(CRect(0, 0, 120, 285), -1, "", kCenterText, 14); +view__29->addView(view__30); +Label* const view__31 = createLabel(CRect(10, 10, 70, 35), -1, "Curves:", kLeftText, 12); +view__30->addView(view__31); +Label* const view__32 = createLabel(CRect(10, 35, 70, 60), -1, "Masters:", kLeftText, 12); +view__30->addView(view__32); +Label* const view__33 = createLabel(CRect(10, 60, 70, 85), -1, "Groups:", kLeftText, 12); +view__30->addView(view__33); +Label* const view__34 = createLabel(CRect(10, 85, 70, 110), -1, "Regions:", kLeftText, 12); +view__30->addView(view__34); +Label* const view__35 = createLabel(CRect(10, 110, 70, 135), -1, "Samples:", kLeftText, 12); +view__30->addView(view__35); +Label* const view__36 = createLabel(CRect(70, 10, 110, 35), -1, "0", kCenterText, 12); +infoCurvesLabel_ = view__36; +view__30->addView(view__36); +Label* const view__37 = createLabel(CRect(70, 35, 110, 60), -1, "0", kCenterText, 12); +infoMastersLabel_ = view__37; +view__30->addView(view__37); +Label* const view__38 = createLabel(CRect(70, 60, 110, 85), -1, "0", kCenterText, 12); +infoGroupsLabel_ = view__38; +view__30->addView(view__38); +Label* const view__39 = createLabel(CRect(70, 85, 110, 110), -1, "0", kCenterText, 12); +infoRegionsLabel_ = view__39; +view__30->addView(view__39); +Label* const view__40 = createLabel(CRect(70, 110, 110, 135), -1, "0", kCenterText, 12); +infoSamplesLabel_ = view__40; +view__30->addView(view__40); +LogicalGroup* const view__41 = createLogicalGroup(CRect(125, 0, 790, 280), -1, "", kCenterText, 14); +view__29->addView(view__41); +SfizzLargePicture* const view__42 = createSfizzLargePicture(CRect(130, 15, 530, 265), -1, "", kCenterText, 14); +view__41->addView(view__42); +LogicalGroup* const view__43 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelControls] = view__43; +view__0->addView(view__43); +view__43->setVisible(false); +RoundedGroup* const view__44 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); +view__43->addView(view__44); +Label* const view__45 = createLabel(CRect(0, 0, 790, 285), -1, "Controls not available", kCenterText, 40); +view__44->addView(view__45); +LogicalGroup* const view__46 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelSettings] = view__46; +view__0->addView(view__46); +TitleGroup* const view__47 = createTitleGroup(CRect(255, 15, 535, 125), -1, "Engine", kCenterText, 12); +view__46->addView(view__47); +ValueMenu* const view__48 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); +numVoicesSlider_ = view__48; +view__47->addView(view__48); +ValueLabel* const view__49 = createValueLabel(CRect(15, 20, 95, 45), -1, "Polyphony", kCenterText, 12); +view__47->addView(view__49); +ValueMenu* const view__50 = createValueMenu(CRect(110, 60, 170, 85), kTagSetOversampling, "", kCenterText, 12); +oversamplingSlider_ = view__50; +view__47->addView(view__50); +ValueLabel* const view__51 = createValueLabel(CRect(100, 20, 180, 45), -1, "Oversampling", kCenterText, 12); +view__47->addView(view__51); +ValueLabel* const view__52 = createValueLabel(CRect(185, 20, 265, 45), -1, "Preload size", kCenterText, 12); +view__47->addView(view__52); +ValueMenu* const view__53 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); +preloadSizeSlider_ = view__53; +view__47->addView(view__53); +TitleGroup* const view__54 = createTitleGroup(CRect(200, 150, 590, 270), -1, "Tuning", kCenterText, 12); +view__46->addView(view__54); +ValueLabel* const view__55 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); +view__54->addView(view__55); +ValueMenu* const view__56 = createValueMenu(CRect(220, 60, 280, 85), kTagSetTuningFrequency, "", kCenterText, 12); +tuningFrequencySlider_ = view__56; +view__54->addView(view__56); +ValueLabel* const view__57 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); +view__54->addView(view__57); +Knob48* const view__58 = createKnob48(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); +stretchedTuningSlider_ = view__58; +view__54->addView(view__58); +ValueLabel* const view__59 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); +view__54->addView(view__59); +ValueLabel* const view__60 = createValueLabel(CRect(20, 20, 120, 45), -1, "Scala file", kCenterText, 12); +view__54->addView(view__60); +ValueButton* const view__61 = createValueButton(CRect(20, 60, 120, 85), kTagLoadScalaFile, "DefaultScale", kCenterText, 12); +scalaFileButton_ = view__61; +view__54->addView(view__61); +ValueMenu* const view__62 = createValueMenu(CRect(135, 60, 170, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootKeySlider_ = view__62; +view__54->addView(view__62); +ValueMenu* const view__63 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootOctaveSlider_ = view__63; +view__54->addView(view__63); +LogicalGroup* const view__64 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelInfo] = view__64; +view__0->addView(view__64); +view__64->setVisible(false); +RoundedGroup* const view__65 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); +view__64->addView(view__65); +Label* const view__66 = createLabel(CRect(0, 0, 790, 285), -1, "Informative text goes here", kCenterText, 40); +view__65->addView(view__66); +Piano* const view__67 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); +view__0->addView(view__67); diff --git a/editor/tools/layout-maker/LICENSE b/editor/tools/layout-maker/LICENSE new file mode 100644 index 00000000..36b7cd93 --- /dev/null +++ b/editor/tools/layout-maker/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/editor/tools/layout-maker/README b/editor/tools/layout-maker/README new file mode 100644 index 00000000..e8402cc3 --- /dev/null +++ b/editor/tools/layout-maker/README @@ -0,0 +1,2 @@ +This purpose of this tool is to accept UI designs make with Fluid, the FLTK +design editor, and convert these designs to music plugin interfaces. diff --git a/editor/tools/layout-maker/sources/layout.h b/editor/tools/layout-maker/sources/layout.h new file mode 100644 index 00000000..35a1978e --- /dev/null +++ b/editor/tools/layout-maker/sources/layout.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include + +struct LayoutImage { + std::string filepath; + int x = 0; + int y = 0; + int w = 0; + int h = 0; +}; + +struct LayoutItem { + std::string id; + std::string classname; + std::string label; + int x = 0; + int y = 0; + int w = 0; + int h = 0; + std::string box; + std::string down_box; + int labelfont = 0; + int labelsize = 14; + std::string labeltype; + int textsize = 14; + int align = 0; + double value = 0; + double minimum = 0; + double maximum = 0; + double step = 0; + std::string type; + std::string callback; + LayoutImage image; + bool hidden = false; + std::string comment; + std::vector items; +}; + +struct Layout { + std::vector items; +}; diff --git a/editor/tools/layout-maker/sources/main.cpp b/editor/tools/layout-maker/sources/main.cpp new file mode 100644 index 00000000..06e5283b --- /dev/null +++ b/editor/tools/layout-maker/sources/main.cpp @@ -0,0 +1,136 @@ +#include "layout.h" +#include "reader.h" +#include +#include +#include +#include + +/// +typedef std::unordered_map Metadata; + +static Metadata metadata_from_comment(absl::string_view comment) +{ + Metadata md; + + while (!comment.empty()) { + absl::string_view line; + + size_t pos = comment.find_first_of("\r\n"); + if (pos != comment.npos) { + line = comment.substr(0, pos); + comment.remove_prefix(pos + 1); + } + else { + line = comment; + comment = {}; + } + + line = absl::StripAsciiWhitespace(line); + if (line.empty() || line[0] == '#') + continue; + + std::string key, value; + pos = line.find_first_of('='); + if (pos != comment.npos) { + key = std::string(line.substr(0, pos)); + value = std::string(line.substr(pos + 1)); + } + else + key = std::string(line); + + md.emplace(std::move(key), std::move(value)); + } + + return md; +} + +/// +static void codegen_item(int& idCounter, int parentId, int parentX, int parentY, const LayoutItem& item, absl::string_view oldTheme) +{ + const Metadata md = metadata_from_comment(item.comment); + + absl::string_view tag = "-1"; + absl::string_view newTheme; + + Metadata::const_iterator it; + it = md.find("tag"); + if (it != md.end()) + tag = it->second; + it = md.find("theme"); + if (it != md.end()) + newTheme = it->second; + + absl::string_view currentTheme = newTheme.empty() ? oldTheme : newTheme; + + int id = idCounter++; + int myX = item.x; + int myY = item.y; + if (parentId == -1) { + myX = 0; + myY = 0; + } + int relX = myX - parentX; + int relY = myY - parentY; + + //std::cout << "// Begin " << id << " " << item.classname << " {" << item.label << "}" << "\n"; + + if (!newTheme.empty()) + std::cout << "enterTheme(" << newTheme << ");\n"; + + absl::string_view label; + if (!item.label.empty() && item.labeltype != "NO_LABEL") + label = item.label; + + absl::string_view align = "kCenterText"; + if (item.align & 4) + align = "kLeftText"; + else if (item.align & 8) + align = "kRightText"; + + std::cout << item.classname << "* const view__" << id << " = create" << item.classname << "(CRect(" << relX << ", " << relY << ", " << (relX + item.w) << ", " << (relY + item.h) << "), " << tag << ", \"" << label << "\", " << align << ", " << item.labelsize << ");\n"; + + if (!item.id.empty()) + std::cout << item.id << " = view__" << id << ";\n"; + + if (parentId != -1) + std::cout << "view__" << parentId << "->addView(view__" << id << ");\n"; + + if (item.hidden) + std::cout << "view__" << id << "->setVisible(false);\n"; + + for (const LayoutItem& subItem : item.items) + codegen_item(idCounter, id, myX, myY, subItem, currentTheme); + + if (!newTheme.empty()) + std::cout << "enterTheme(" << oldTheme << ");\n"; + + //std::cout << "// End " << id << " " << item.classname << " {" << item.label << "}" << "\n"; +} + +static void codegen_layout(const LayoutItem& item) +{ + int idCounter = 0; + codegen_item(idCounter, -1, 0, 0, item, "defaultTheme"); +} + +/// +int main(int argc, char *argv[]) +{ + if (argc != 2) { + std::cerr << "Please indicate a fluid design file.\n"; + return 1; + } + + Layout layout = read_file_layout(argv[1]); + + if (layout.items.size() != 1) { + std::cerr << "There must be exactly 1 top level component."; + return 1; + } + + std::cout << "/* This file is generated by the layout maker tool. */\n"; + + codegen_layout(layout.items[0]); + + return 0; +} diff --git a/editor/tools/layout-maker/sources/reader.cpp b/editor/tools/layout-maker/sources/reader.cpp new file mode 100644 index 00000000..788f93d7 --- /dev/null +++ b/editor/tools/layout-maker/sources/reader.cpp @@ -0,0 +1,359 @@ +#include "reader.h" +#include +#include +#include + +typedef std::vector TokenList; +static bool read_file_tokens(const char *filename, TokenList &tokens); +static Layout read_tokens_layout(TokenList::iterator &tok_it, TokenList::iterator tok_end); + +Layout read_file_layout(const char *filename) +{ + std::vector tokens; + if (!read_file_tokens(filename, tokens)) + throw std::runtime_error("Cannot read fluid design file."); + + TokenList::iterator tok_it = tokens.begin(); + TokenList::iterator tok_end = tokens.end(); + return read_tokens_layout(tok_it, tok_end); +} + +static std::string consume_next_token(TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + if (tok_it == tok_end) + throw file_format_error("Premature end of tokens"); + return *tok_it++; +} + +static bool try_consume_next_token(const char *text, TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + if (tok_it == tok_end) + return false; + + if (*tok_it != text) + return false; + + ++tok_it; + return true; +} + +static void ensure_next_token(const char *text, TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + std::string tok = consume_next_token(tok_it, tok_end); + if (tok != text) + throw file_format_error("Unexpected token: " + tok); +} + +static std::string consume_enclosed_string(TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + ensure_next_token("{", tok_it, tok_end); + unsigned depth = 1; + + std::string text; + for (;;) { + std::string part = consume_next_token(tok_it, tok_end); + if (part == "}") { + if (--depth == 0) + return text; + } + else if (part == "{") + ++depth; + if (!text.empty()) + text.push_back(' '); + text.append(part); + } + + return text; +} + +static std::string consume_any_string(TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + if (tok_it != tok_end && *tok_it == "{") + return consume_enclosed_string(tok_it, tok_end); + else + return consume_next_token(tok_it, tok_end); +} + +static int consume_int_token(TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + std::string text = consume_next_token(tok_it, tok_end); + return std::stoi(text); +} + +static int consume_real_token(TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + std::string text = consume_next_token(tok_it, tok_end); + return std::stod(text); +} + +static void consume_image_properties(LayoutImage &image, TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + for (bool have = true; have;) { + if (try_consume_next_token("xywh", tok_it, tok_end)) { + ensure_next_token("{", tok_it, tok_end); + image.x = consume_int_token(tok_it, tok_end); + image.y = consume_int_token(tok_it, tok_end); + image.w = consume_int_token(tok_it, tok_end); + image.h = consume_int_token(tok_it, tok_end); + ensure_next_token("}", tok_it, tok_end); + } + else + have = false; + } +} + +// static void consume_layout_item_properties(LayoutItem &item, TokenList::iterator &tok_it, TokenList::iterator tok_end) +// { +// ensure_next_token("{", tok_it, tok_end); +// for (std::string text; (text = consume_next_token(tok_it, tok_end)) != "}";) { +// if (text == "open" || text == "selected") +// ; // skip +// else if (text == "label") +// item.label = consume_any_string(tok_it, tok_end); +// else if (text == "xywh") { +// ensure_next_token("{", tok_it, tok_end); +// item.x = consume_int_token(tok_it, tok_end); +// item.y = consume_int_token(tok_it, tok_end); +// item.w = consume_int_token(tok_it, tok_end); +// item.h = consume_int_token(tok_it, tok_end); +// ensure_next_token("}", tok_it, tok_end); +// } +// else if (text == "box") +// item.box = consume_next_token(tok_it, tok_end); +// else if (text == "labelfont") +// item.labelfont = consume_int_token(tok_it, tok_end); +// else if (text == "labelsize") +// item.labelsize = consume_int_token(tok_it, tok_end); +// else if (text == "labeltype") +// item.labeltype = consume_any_string(tok_it, tok_end); +// else if (text == "align") +// item.align = consume_int_token(tok_it, tok_end); +// else if (text == "type") +// item.type = consume_any_string(tok_it, tok_end); +// else if (text == "callback") +// item.callback = consume_any_string(tok_it, tok_end); +// else if (text == "class") +// item.classname = consume_any_string(tok_it, tok_end); +// else if (text == "minimum") +// item.minimum = consume_real_token(tok_it, tok_end); +// else if (text == "maximum") +// item.maximum = consume_real_token(tok_it, tok_end); +// else if (text == "step") +// item.step = consume_real_token(tok_it, tok_end); +// else if (text == "image") { +// item.image.filepath = consume_any_string(tok_it, tok_end); +// consume_image_properties(item.image, tok_it, tok_end); +// } +// } +// } + +static void consume_layout_item_properties(LayoutItem &item, TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + ensure_next_token("{", tok_it, tok_end); + for (bool have = true; have;) { + if (try_consume_next_token("open", tok_it, tok_end)) + ; // skip + else if (try_consume_next_token("selected", tok_it, tok_end)) + ; // skip + else if (try_consume_next_token("label", tok_it, tok_end)) + item.label = consume_any_string(tok_it, tok_end); + else if (try_consume_next_token("xywh", tok_it, tok_end)) { + ensure_next_token("{", tok_it, tok_end); + item.x = consume_int_token(tok_it, tok_end); + item.y = consume_int_token(tok_it, tok_end); + item.w = consume_int_token(tok_it, tok_end); + item.h = consume_int_token(tok_it, tok_end); + ensure_next_token("}", tok_it, tok_end); + } + else if (try_consume_next_token("box", tok_it, tok_end)) + item.box = consume_next_token(tok_it, tok_end); + else if (try_consume_next_token("down_box", tok_it, tok_end)) + item.down_box = consume_next_token(tok_it, tok_end); + else if (try_consume_next_token("labelfont", tok_it, tok_end)) + item.labelfont = consume_int_token(tok_it, tok_end); + else if (try_consume_next_token("labelsize", tok_it, tok_end)) + item.labelsize = consume_int_token(tok_it, tok_end); + else if (try_consume_next_token("labeltype", tok_it, tok_end)) + item.labeltype = consume_any_string(tok_it, tok_end); + else if (try_consume_next_token("textsize", tok_it, tok_end)) + item.textsize = consume_int_token(tok_it, tok_end); + else if (try_consume_next_token("align", tok_it, tok_end)) + item.align = consume_int_token(tok_it, tok_end); + else if (try_consume_next_token("type", tok_it, tok_end)) + item.type = consume_any_string(tok_it, tok_end); + else if (try_consume_next_token("callback", tok_it, tok_end)) + item.callback = consume_any_string(tok_it, tok_end); + else if (try_consume_next_token("class", tok_it, tok_end)) + item.classname = consume_any_string(tok_it, tok_end); + else if (try_consume_next_token("value", tok_it, tok_end)) + item.value = consume_real_token(tok_it, tok_end); + else if (try_consume_next_token("minimum", tok_it, tok_end)) + item.minimum = consume_real_token(tok_it, tok_end); + else if (try_consume_next_token("maximum", tok_it, tok_end)) + item.maximum = consume_real_token(tok_it, tok_end); + else if (try_consume_next_token("step", tok_it, tok_end)) + item.step = consume_real_token(tok_it, tok_end); + else if (try_consume_next_token("image", tok_it, tok_end)) + item.image.filepath = consume_any_string(tok_it, tok_end); + else if (try_consume_next_token("hide", tok_it, tok_end)) + item.hidden = true; + else if (try_consume_next_token("visible", tok_it, tok_end)) + /* skip */; + else if (try_consume_next_token("comment", tok_it, tok_end)) + item.comment = consume_any_string(tok_it, tok_end); + else + have = false; + } + ensure_next_token("}", tok_it, tok_end); +} + +static LayoutItem consume_layout_item(const std::string &classname, TokenList::iterator &tok_it, TokenList::iterator tok_end, bool anonymous = false) +{ + LayoutItem item; + item.classname = classname; + if (!anonymous) + item.id = consume_any_string(tok_it, tok_end); + consume_layout_item_properties(item, tok_it, tok_end); + if (tok_it != tok_end && *tok_it == "{") { + consume_next_token(tok_it, tok_end); + for (std::string text; (text = consume_next_token(tok_it, tok_end)) != "}";) { + if (text == "decl") { + consume_any_string(tok_it, tok_end); + consume_any_string(tok_it, tok_end); + } + else if (text == "Function") { + consume_any_string(tok_it, tok_end); + consume_any_string(tok_it, tok_end); + consume_any_string(tok_it, tok_end); + } + else + item.items.push_back(consume_layout_item(text, tok_it, tok_end)); + } + } + return item; +} + +static Layout read_tokens_layout(TokenList::iterator &tok_it, TokenList::iterator tok_end) +{ + Layout layout; + + std::string version_name; + std::string header_name; + std::string code_name; + + while (tok_it != tok_end) { + std::string key = consume_next_token(tok_it, tok_end); + + if (key == "version") + version_name = consume_next_token(tok_it, tok_end); + else if (key == "header_name") { + ensure_next_token("{", tok_it, tok_end); + header_name = consume_next_token(tok_it, tok_end); + ensure_next_token("}", tok_it, tok_end); + } + else if (key == "code_name") { + ensure_next_token("{", tok_it, tok_end); + code_name = consume_next_token(tok_it, tok_end); + ensure_next_token("}", tok_it, tok_end); + } + else if (key == "decl") { + consume_any_string(tok_it, tok_end); + consume_any_string(tok_it, tok_end); + } + else if (key == "widget_class") { + key = consume_next_token(tok_it, tok_end); + layout.items.push_back(consume_layout_item(key, tok_it, tok_end, true)); + layout.items.back().id = key; + } + else + layout.items.push_back(consume_layout_item(key, tok_it, tok_end)); + } + + return layout; +} + +/// +class tokenizer { +public: + tokenizer( + absl::string_view text, + absl::string_view dropped_delims, + absl::string_view kept_delims); + + absl::string_view next(); + +private: + absl::string_view text_; + absl::string_view dropped_delims_; + absl::string_view kept_delims_; +}; + +tokenizer::tokenizer( + absl::string_view text, + absl::string_view dropped_delims, + absl::string_view kept_delims) + : text_(text), dropped_delims_(dropped_delims), kept_delims_(kept_delims) +{ +} + +absl::string_view tokenizer::next() +{ + auto is_dropped = [this](char c) -> bool { + return dropped_delims_.find(c) != dropped_delims_.npos; + }; + auto is_kept = [this](char c) -> bool { + return kept_delims_.find(c) != kept_delims_.npos; + }; + auto is_delim = [this](char c) -> bool { + return dropped_delims_.find(c) != dropped_delims_.npos || + kept_delims_.find(c) != kept_delims_.npos; + }; + + absl::string_view text = text_; + + while (!text.empty() && is_dropped(text[0])) + text.remove_prefix(1); + + if (text.empty()) + return {}; + + size_t pos; + { + auto it = std::find_if(text.begin(), text.end(), is_delim); + if (it == text.end()) + pos = text.size(); + else { + pos = std::distance(text.begin(), it); + pos += is_kept(text[0]); + } + } + + absl::string_view token = text.substr(0, pos); + text_ = text.substr(pos); + return token; +} + +/// +static bool read_file_tokens(const char *filename, TokenList &tokens) +{ + std::ifstream stream(filename); + std::string line; + + std::string text; + while (std::getline(stream, line)) { + if (!line.empty() && line[0] != '#') { + text.append(line); + text.push_back('\n'); + } + } + + if (stream.bad()) + return false; + + tokenizer tok(text, " \t\r\n", "{}"); + absl::string_view token; + while (!(token = tok.next()).empty()) + tokens.emplace_back(token); + + return !stream.bad(); +} diff --git a/editor/tools/layout-maker/sources/reader.h b/editor/tools/layout-maker/sources/reader.h new file mode 100644 index 00000000..589bd307 --- /dev/null +++ b/editor/tools/layout-maker/sources/reader.h @@ -0,0 +1,13 @@ +#pragma once +#include "layout.h" +#include +#include + +Layout read_file_layout(const char *filename); + +/// +struct file_format_error : public std::runtime_error { +public: + explicit file_format_error(const std::string &reason = "Format error") + : runtime_error(reason) {} +}; diff --git a/lv2/CMakeLists.txt b/lv2/CMakeLists.txt index 3322d3d1..a888ca81 100644 --- a/lv2/CMakeLists.txt +++ b/lv2/CMakeLists.txt @@ -98,10 +98,9 @@ endforeach() if (SFIZZ_LV2_UI) execute_process ( COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/Contents/Resources") - foreach(res ${EDITOR_RESOURCES}) - file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources/${res}" - DESTINATION "${PROJECT_BINARY_DIR}/Contents/Resources") - endforeach() + copy_editor_resources( + "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources" + "${PROJECT_BINARY_DIR}/Contents/Resources") endif() # Installation diff --git a/scripts/innosetup.iss.in b/scripts/innosetup.iss.in index b287de6a..7c845a4b 100644 --- a/scripts/innosetup.iss.in +++ b/scripts/innosetup.iss.in @@ -50,9 +50,7 @@ Name: "vst3"; Description: "VST3 plugin"; Types: full custom; [Files] Source: "sfizz.lv2\Contents\Binary\sfizz.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Binary"; Flags: ignoreversion Source: "sfizz.lv2\Contents\Binary\sfizz_ui.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Binary"; Flags: ignoreversion -Source: "sfizz.lv2\Contents\Resources\DefaultInstrument.sfz"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" -Source: "sfizz.lv2\Contents\Resources\DefaultScale.scl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" -Source: "sfizz.lv2\Contents\Resources\logo.png"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" +Source: "sfizz.lv2\Contents\Resources\*"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" Source: "sfizz.lv2\manifest.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\sfizz.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\sfizz_ui.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" @@ -60,7 +58,7 @@ Source: "sfizz.lv2\lgpl-3.0.txt"; Components: main; DestDir: "{app}" Source: "sfizz.lv2\LICENSE.md"; Components: main; DestDir: "{app}" Source: "sfizz.vst3\desktop.ini"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3" Source: "sfizz.vst3\Contents\@VST3_PACKAGE_ARCHITECTURE@-win\sfizz.vst3"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3\Contents\@VST3_PACKAGE_ARCHITECTURE@-win"; Flags: ignoreversion -Source: "sfizz.vst3\Contents\Resources\logo.png"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3\Contents\Resources" +Source: "sfizz.vst3\Contents\Resources\*"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3\Contents\Resources" Source: "sfizz.vst3\Plugin.ico"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3" Source: "sfizz.vst3\gpl-3.0.txt"; Components: main; DestDir: "{app}" ;Source: "setup\vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index c4b3538e..bbd2ff2e 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -68,10 +68,9 @@ endif() # Create the bundle (see "VST 3 Locations / Format") execute_process ( COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") -foreach(res ${EDITOR_RESOURCES}) - file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources/${res}" - DESTINATION "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") -endforeach() +copy_editor_resources( + "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources" + "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") if(WIN32) set_target_properties(${VSTPLUGIN_PRJ_NAME} PROPERTIES SUFFIX ".vst3" From e3de5d245bdc50e8c6e4abe5bc9779a689ac5bbc Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 7 Sep 2020 13:37:50 +0200 Subject: [PATCH 183/445] Attempt to fix MSVC and the layout tool --- editor/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index bd9a38a8..cce49479 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -74,7 +74,7 @@ if(NOT CMAKE_CROSSCOMPILING) add_custom_command( OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/editor/layout/main.hpp" - COMMAND "${CMAKE_CURRENT_BINARY_DIR}/layout-maker" + COMMAND "$" "${CMAKE_CURRENT_SOURCE_DIR}/layout/main.fl" > "${CMAKE_CURRENT_SOURCE_DIR}/src/editor/layout/main.hpp" DEPENDS layout-maker "${CMAKE_CURRENT_SOURCE_DIR}/layout/main.fl") From c2a82afdcea6fba7825719a3d9859b6d94927749 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 7 Sep 2020 13:46:28 +0200 Subject: [PATCH 184/445] appveyor: vstgui wants a Windows SDK update --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ebc8dc20..3bb7f7f6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ version: build-{build} -image: Visual Studio 2017 +image: Visual Studio 2019 configuration: Release platform: - Win32 @@ -22,7 +22,7 @@ before_build: - cmd: git submodule update --init - cmd: mkdir CMakeBuild - cmd: cd CMakeBuild -- cmd: cmake .. -G"Visual Studio 15 2017" -A"%platform%" -DSFIZZ_JACK=OFF -DSFIZZ_BENCHMARKS=OFF -DSFIZZ_TESTS=OFF -DSFIZZ_LV2=ON -DSFIZZ_VST=ON -DCMAKE_BUILD_TYPE=Release -DVCPKG_TARGET_TRIPLET=%VCPKG_TRIPLET% -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake +- cmd: cmake .. -G"Visual Studio 16 2019" -A"%platform%" -DSFIZZ_JACK=OFF -DSFIZZ_BENCHMARKS=OFF -DSFIZZ_TESTS=OFF -DSFIZZ_LV2=ON -DSFIZZ_VST=ON -DCMAKE_BUILD_TYPE=Release -DVCPKG_TARGET_TRIPLET=%VCPKG_TRIPLET% -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake build_script: - cmd: cmake --build . --config Release -j From fc036c8e3bb15ceb849903814f231330addd691a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 7 Sep 2020 14:10:20 +0200 Subject: [PATCH 185/445] Ensure to use utf8 coded literals --- editor/src/editor/Editor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 8e3089a8..794eb331 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -496,10 +496,10 @@ void Editor::Impl::createFrameContents() return btn; }; auto createLoadFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { - return createGlyphButton("\ue142", bounds, tag, fontsize); + return createGlyphButton(u8"\ue142", bounds, tag, fontsize); }; auto createEditFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { - return createGlyphButton("\ue148", bounds, tag, fontsize); + return createGlyphButton(u8"\ue148", bounds, tag, fontsize); }; auto createPiano = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { SPiano* piano = new SPiano(bounds); From a285d387d7c09fa41b225539a5bbe9e3de9c7d7c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 7 Sep 2020 05:52:18 -0700 Subject: [PATCH 186/445] Allow the AudioUnit to build again --- cmake/SfizzConfig.cmake | 15 +++++++++++++++ vst/CMakeLists.txt | 1 + 2 files changed, 16 insertions(+) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 270954e8..f9201ec8 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -27,6 +27,21 @@ if (WIN32) add_compile_definitions(NOMINMAX) endif() +# Find macOS system libraries +if(APPLE) + find_library(APPLE_COREFOUNDATION_LIBRARY "CoreFoundation") + find_library(APPLE_FOUNDATION_LIBRARY "Foundation") + find_library(APPLE_COCOA_LIBRARY "Cocoa") + find_library(APPLE_CARBON_LIBRARY "Carbon") + find_library(APPLE_OPENGL_LIBRARY "OpenGL") + find_library(APPLE_ACCELERATE_LIBRARY "Accelerate") + find_library(APPLE_QUARTZCORE_LIBRARY "QuartzCore") + find_library(APPLE_AUDIOTOOLBOX_LIBRARY "AudioToolbox") + find_library(APPLE_AUDIOUNIT_LIBRARY "AudioUnit") + find_library(APPLE_COREAUDIO_LIBRARY "CoreAudio") + find_library(APPLE_COREMIDI_LIBRARY "CoreMIDI") +endif() + # The variable CMAKE_SYSTEM_PROCESSOR is incorrect on Visual studio... # see https://gitlab.kitware.com/cmake/cmake/issues/15170 diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index bbd2ff2e..cddf56b2 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -144,6 +144,7 @@ elseif(SFIZZ_AU) "${APPLE_COCOA_LIBRARY}" "${APPLE_CARBON_LIBRARY}" "${APPLE_AUDIOTOOLBOX_LIBRARY}" + "${APPLE_AUDIOUNIT_LIBRARY}" "${APPLE_COREAUDIO_LIBRARY}" "${APPLE_COREMIDI_LIBRARY}") From ed5fb61f3831dc3e39fb6646c1fdad1cca45c279 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 10:56:12 +0200 Subject: [PATCH 187/445] Move a variable declaration Refactor the polyphony checks into reusable blocks --- src/sfizz/Synth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 5a0b6103..4903a644 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -979,7 +979,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); } - auto parent = region->parent; // Polyphony reached on region if (regionPolyphonyArray.size() >= region->polyphony) { @@ -995,6 +994,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } // Polyphony reached some parent group/master/etc + auto parent = region->parent; while (parent != nullptr) { if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) { const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); From ca7d74e8411a0769bb683bda48eb664135d61edd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 19:12:03 +0200 Subject: [PATCH 188/445] Move the voice selection after the polyphony checks --- src/sfizz/Synth.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 4903a644..e6da53ad 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -932,16 +932,9 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc if (region->registerNoteOn(noteNumber, velocity, randValue)) { unsigned notePolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; - Voice* selectedVoice { nullptr }; regionPolyphonyArray.clear(); for (auto& voice : voices) { - if (voice->isFree()) { - if (selectedVoice == nullptr) - selectedVoice = voice.get(); - continue; - } - if (voice->getRegion() == region && !voice->releasedOrFree()) { regionPolyphonyArray.push_back(voice.get()); } @@ -1003,6 +996,8 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc parent = parent->getParent(); } + Voice* selectedVoice = findFreeVoice(); + // Engine polyphony reached, we're stealing something if (selectedVoice == nullptr) { selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); From 5139c648f0dab00c3b9c600bbe5091e460436efa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 19:31:47 +0200 Subject: [PATCH 189/445] Engine polyphony is checked in findFreeVoice() --- src/sfizz/Synth.cpp | 29 ++++++++++++----------------- src/sfizz/Synth.h | 5 ++++- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e6da53ad..09fa9736 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -659,7 +659,18 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); - return {}; + // Engine polyphony reached + Voice* stolenVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); + if (stolenVoice == nullptr) + return {}; + + auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); + SisterVoiceRing::applyToRing(stolenVoice, [&] (Voice* v) { + renderVoiceToOutputs(*v, *tempSpan); + v->reset(); + }); + + return stolenVoice; } int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept @@ -997,27 +1008,11 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } Voice* selectedVoice = findFreeVoice(); - - // Engine polyphony reached, we're stealing something - if (selectedVoice == nullptr) { - selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); - } - // For some reason we did not find a voice to use. // This is a degraded case but we'll just drop the note on. if (selectedVoice == nullptr) continue; - // Kill voice if necessary, pre-rendering it into the output buffers - if (!selectedVoice->isFree()) { - auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); - SisterVoiceRing::applyToRing(selectedVoice, [&] (Voice* v) { - renderVoiceToOutputs(*v, *tempSpan); - v->reset(); - }); - } - - // Voice should be free now ASSERT(selectedVoice->isFree()); selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); ring.addVoiceToRing(selectedVoice); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3fb85f7f..7dd3b0ad 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -765,18 +765,21 @@ private: using RegionSetPtr = std::unique_ptr; std::vector regions; std::vector voices; + // These are more general "groups" than sfz and encapsulates the full hierarchy RegionSet* currentSet; OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal }; std::vector sets; + // These are the `group=` groups where you can off voices std::vector polyphonyGroups; + // Views to speed up iteration over the regions and voices when events // occur in the audio callback VoiceViewVector regionPolyphonyArray; + VoiceViewVector voiceViewArray; VoiceStealing stealer; - VoiceViewVector voiceViewArray; std::array noteActivationLists; std::array ccActivationLists; From ff1be61dc1142ef469888eb3a6a4cf777934acd2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 19:36:49 +0200 Subject: [PATCH 190/445] Use the same idiom for finding free voices --- src/sfizz/Synth.cpp | 50 +++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 09fa9736..2e9f1f41 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -922,14 +922,13 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex continue; } - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + if (Voice* selectedVoice = findFreeVoice()) { + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); + } } } } @@ -1007,17 +1006,13 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc parent = parent->getParent(); } - Voice* selectedVoice = findFreeVoice(); - // For some reason we did not find a voice to use. - // This is a degraded case but we'll just drop the note on. - if (selectedVoice == nullptr) - continue; - - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); + if (Voice* selectedVoice = findFreeVoice()) { + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); + } } } } @@ -1090,16 +1085,13 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept } if (region->registerCC(ccNumber, normValue)) { - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - - voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); - - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + if (Voice* selectedVoice = findFreeVoice()) { + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); + } } } } From afae38e6c3ab05b8148ee34a15b949c99e2cc075 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 20:09:45 +0200 Subject: [PATCH 191/445] Move the polyphony checks in separate functions --- src/sfizz/Synth.cpp | 153 ++++++++++++++++++++++++++------------------ src/sfizz/Synth.h | 7 +- 2 files changed, 98 insertions(+), 62 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 2e9f1f41..f334136a 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -933,6 +933,92 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex } } +void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept +{ + tempPolyphonyArray.clear(); + + for (Voice* voice : voiceViewArray) { + if (voice->getRegion() == region && !voice->releasedOrFree()) { + tempPolyphonyArray.push_back(voice); + } + } + + if (tempPolyphonyArray.size() >= region->polyphony) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } +} + +void sfz::Synth::checkNotePolyphony(const Region* region, int delay, int number, float value, Voice::TriggerType triggerType) noexcept +{ + if (!region->notePolyphony) + return; + + unsigned notePolyphonyCounter { 0 }; + Voice* selfMaskCandidate { nullptr }; + + for (Voice* voice : voiceViewArray) { + if (!voice->releasedOrFree() + && voice->getRegion()->group == region->group + && voice->getTriggerNumber() == number + && voice->getTriggerType() ==triggerType) { + notePolyphonyCounter += 1; + switch (region->selfMask) { + case SfzSelfMask::mask: + if (voice->getTriggerValue() <= value) { + if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) + selfMaskCandidate = voice; + } + break; + case SfzSelfMask::dontMask: + if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition()) + selfMaskCandidate = voice; + break; + } + } + } + + if (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) + SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); +} + +void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept +{ + const auto& activeVoices = polyphonyGroups[region->group].getActiveVoices(); + tempPolyphonyArray.clear(); + for (Voice* voice : activeVoices) { + if (!voice->releasedOrFree()) { + tempPolyphonyArray.push_back(voice); + } + } + + if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } +} + +void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept +{ + auto parent = region->parent; + while (parent != nullptr) { + const auto& activeVoices = parent->getActiveVoices(); + tempPolyphonyArray.clear(); + for (Voice* voice : activeVoices) { + if (!voice->releasedOrFree()) { + tempPolyphonyArray.push_back(voice); + } + } + + if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } + + parent = parent->getParent(); + } +} + void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); @@ -940,71 +1026,16 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - unsigned notePolyphonyCounter { 0 }; - Voice* selfMaskCandidate { nullptr }; - regionPolyphonyArray.clear(); for (auto& voice : voices) { - if (voice->getRegion() == region && !voice->releasedOrFree()) { - regionPolyphonyArray.push_back(voice.get()); - } - - if (region->notePolyphony) { - if (!voice->releasedOrFree() - && voice->getRegion()->group == region->group - && voice->getTriggerNumber() == noteNumber - && voice->getTriggerType() == Voice::TriggerType::NoteOn) { - notePolyphonyCounter += 1; - switch (region->selfMask) { - case SfzSelfMask::mask: - if (voice->getTriggerValue() <= velocity) { - if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) - selfMaskCandidate = voice.get(); - } - break; - case SfzSelfMask::dontMask: - if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition()) - selfMaskCandidate = voice.get(); - break; - } - } - } - if (voice->checkOffGroup(delay, region->group)) noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - // Polyphony reached on note_polyphony - // If there's a self-masking candidate, release it - if (region->notePolyphony - && notePolyphonyCounter >= *region->notePolyphony - && selfMaskCandidate != nullptr) { - SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); - } - - - // Polyphony reached on region - if (regionPolyphonyArray.size() >= region->polyphony) { - const auto activeVoices = absl::MakeSpan(regionPolyphonyArray); - SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); - } - - // Polyphony reached on polyphony group - if (polyphonyGroups[region->group].numPlayingVoices() - == polyphonyGroups[region->group].getPolyphonyLimit()) { - const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); - SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); - } - - // Polyphony reached some parent group/master/etc - auto parent = region->parent; - while (parent != nullptr) { - if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) { - const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); - SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); - } - parent = parent->getParent(); - } + checkNotePolyphony(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + checkRegionPolyphony(region, delay); + checkGroupPolyphony(region, delay); + checkSetPolyphony(region, delay); if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); @@ -1417,8 +1448,8 @@ void sfz::Synth::resetVoices(int numVoices) voiceViewArray.clear(); voiceViewArray.reserve(numVoices); - regionPolyphonyArray.clear(); - regionPolyphonyArray.reserve(numVoices); + tempPolyphonyArray.clear(); + tempPolyphonyArray.reserve(numVoices); for (int i = 0; i < numVoices; ++i) { auto voice = absl::make_unique(i, resources); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 7dd3b0ad..a458e6c8 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -776,10 +776,15 @@ private: // Views to speed up iteration over the regions and voices when events // occur in the audio callback - VoiceViewVector regionPolyphonyArray; + VoiceViewVector tempPolyphonyArray; VoiceViewVector voiceViewArray; VoiceStealing stealer; + void checkRegionPolyphony(const Region* region, int delay) noexcept; + void checkNotePolyphony(const Region* region, int delay, int number, float value, Voice::TriggerType triggerType) noexcept; + void checkGroupPolyphony(const Region* region, int delay) noexcept; + void checkSetPolyphony(const Region* region, int delay) noexcept; + std::array noteActivationLists; std::array ccActivationLists; From dda10e8530174fa79fd3685e21c25fcd790d29e4 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 20:47:51 +0200 Subject: [PATCH 192/445] Use a trigger event type instead of passing 3 parameters --- src/sfizz/Synth.cpp | 42 ++++++++++++++++++++------------- src/sfizz/Synth.h | 2 +- src/sfizz/TriggerEvent.h | 24 +++++++++++++++++++ src/sfizz/Voice.cpp | 38 ++++++++++++++---------------- src/sfizz/Voice.h | 51 +++++++++++++++++----------------------- tests/PolyphonyT.cpp | 46 ++++++++++++++++++------------------ tests/SynthT.cpp | 10 ++++---- 7 files changed, 117 insertions(+), 96 deletions(-) create mode 100644 src/sfizz/TriggerEvent.h diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f334136a..850456b2 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -9,6 +9,7 @@ #include "Debug.h" #include "Macros.h" #include "MidiState.h" +#include "TriggerEvent.h" #include "ModifierHelpers.h" #include "ScopedFTZ.h" #include "StringViewHelpers.h" @@ -892,12 +893,14 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept noteOffDispatch(delay, noteNumber, replacedVelocity); } -bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) { +bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) +{ + const sfz::TriggerEvent& event = voice.getTriggerEvent(); return ( !voice.isFree() - && voice.getTriggerType() == sfz::Voice::TriggerType::NoteOn - && region.keyRange.containsWithEnd(voice.getTriggerNumber()) - && region.velocityRange.containsWithEnd(voice.getTriggerValue()) + && event.type == sfz::TriggerEventType::NoteOn + && region.keyRange.containsWithEnd(event.number) + && region.velocityRange.containsWithEnd(event.value) ); } @@ -905,6 +908,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex { const auto randValue = randNoteDistribution(Random::randomGenerator); SisterVoiceRingBuilder ring; + const TriggerEvent triggerEvent { TriggerEventType::NoteOff, noteNumber, velocity }; for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { @@ -924,7 +928,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); + selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); @@ -949,7 +953,7 @@ void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept } } -void sfz::Synth::checkNotePolyphony(const Region* region, int delay, int number, float value, Voice::TriggerType triggerType) noexcept +void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept { if (!region->notePolyphony) return; @@ -958,15 +962,16 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, int number, Voice* selfMaskCandidate { nullptr }; for (Voice* voice : voiceViewArray) { + const sfz::TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent(); if (!voice->releasedOrFree() && voice->getRegion()->group == region->group - && voice->getTriggerNumber() == number - && voice->getTriggerType() ==triggerType) { + && voiceTriggerEvent.number == triggerEvent.number + && voiceTriggerEvent.type == triggerEvent.type) { notePolyphonyCounter += 1; switch (region->selfMask) { case SfzSelfMask::mask: - if (voice->getTriggerValue() <= value) { - if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) + if (voiceTriggerEvent.value <= triggerEvent.value) { + if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) selfMaskCandidate = voice; } break; @@ -1023,23 +1028,26 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc { const auto randValue = randNoteDistribution(Random::randomGenerator); SisterVoiceRingBuilder ring; + const TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity }; for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { for (auto& voice : voices) { - if (voice->checkOffGroup(delay, region->group)) - noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); + if (voice->checkOffGroup(delay, region->group)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } } - checkNotePolyphony(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + checkNotePolyphony(region, delay, triggerEvent); checkRegionPolyphony(region, delay); checkGroupPolyphony(region, delay); checkSetPolyphony(region, delay); if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); @@ -1082,6 +1090,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept voice->registerCC(delay, ccNumber, normValue); SisterVoiceRingBuilder ring; + const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, normValue }; for (auto& region : ccActivationLists[ccNumber]) { if (ccNumber == region->sustainCC) { @@ -1105,7 +1114,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept if (voice == nullptr) continue; - voice->startVoice(region, delay, note.first, note.second, Voice::TriggerType::NoteOff); + const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; + voice->startVoice(region, delay, noteOffEvent); ring.addVoiceToRing(voice); RegionSet::registerVoiceInHierarchy(region, voice); @@ -1118,7 +1128,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept if (region->registerCC(ccNumber, normValue)) { if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); + selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index a458e6c8..1d2f1f5f 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -781,7 +781,7 @@ private: VoiceStealing stealer; void checkRegionPolyphony(const Region* region, int delay) noexcept; - void checkNotePolyphony(const Region* region, int delay, int number, float value, Voice::TriggerType triggerType) noexcept; + void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept; void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; diff --git a/src/sfizz/TriggerEvent.h b/src/sfizz/TriggerEvent.h new file mode 100644 index 00000000..21108c02 --- /dev/null +++ b/src/sfizz/TriggerEvent.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once + +namespace sfz +{ +enum class TriggerEventType { NoteOn, NoteOff, CC }; + +/** + * @brief Encapsulate a midi event with normalized values + * + */ +struct TriggerEvent +{ + TriggerEventType type; + int number; + float value; +}; + +} diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a6fdf375..9687ebc4 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -36,22 +36,18 @@ sfz::Voice::~Voice() { } -void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept +void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noexcept { - ASSERT(value >= 0.0f && value <= 1.0f); - - if (triggerType == TriggerType::CC) - number = region->pitchKeycenter; - - this->triggerType = triggerType; - triggerNumber = number; - triggerValue = value; + ASSERT(event.value >= 0.0f && event.value <= 1.0f); this->region = region; - if (region->disabled()) return; + triggerEvent = event; + if (triggerEvent.type == TriggerEventType::CC) + triggerEvent.number = region->pitchKeycenter; + switchState(State::playing); ASSERT(delay >= 0); @@ -106,18 +102,18 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, } // do Scala retuning and reconvert the frequency into a 12TET key number - const float numberRetuned = resources.tuning.getKeyFractional12TET(number); + const float numberRetuned = resources.tuning.getKeyFractional12TET(triggerEvent.number); - pitchRatio = region->getBasePitchVariation(numberRetuned, value); + pitchRatio = region->getBasePitchVariation(numberRetuned, triggerEvent.value); // apply stretch tuning if set if (resources.stretch) pitchRatio *= resources.stretch->getRatioForFractionalKey(numberRetuned); - baseVolumedB = region->getBaseVolumedB(number); + baseVolumedB = region->getBaseVolumedB(triggerEvent.number); baseGain = region->getBaseGain(); - if (triggerType != TriggerType::CC) - baseGain *= region->getNoteGain(number, value); + if (triggerEvent.type != TriggerEventType::CC) + baseGain *= region->getNoteGain(triggerEvent.number, triggerEvent.value); gainSmoother.reset(); resetCrossfades(); @@ -127,13 +123,13 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, const unsigned numChannels = region->isStereo() ? 2 : 1; for (auto& filter: region->filters) { - auto newFilter = resources.filterPool.getFilter(filter, numChannels, number, value); + auto newFilter = resources.filterPool.getFilter(filter, numChannels, triggerEvent.number, triggerEvent.value); if (newFilter) filters.push_back(newFilter); } for (auto& eq: region->equalizers) { - auto newEQ = resources.eqPool.getEQ(eq, numChannels, value); + auto newEQ = resources.eqPool.getEQ(eq, numChannels, triggerEvent.value); if (newEQ) equalizers.push_back(newEQ); } @@ -141,11 +137,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sourcePosition = region->getOffset(); triggerDelay = delay; initialDelay = delay + static_cast(region->getDelay() * sampleRate); - baseFrequency = resources.tuning.getFrequencyOfKey(number); + baseFrequency = resources.tuning.getFrequencyOfKey(triggerEvent.number); bendStepFactor = centsFactor(region->bendStep); bendSmoother.setSmoothing(region->bendSmooth, sampleRate); bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); - egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate); + egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate); resources.modMatrix.initVoice(id, region->getId(), delay); } @@ -197,7 +193,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex if (state != State::playing) return; - if (triggerNumber == noteNumber) { + if (triggerEvent.number == noteNumber) { noteIsOff = true; if (region->loopMode == SfzLoopMode::one_shot) @@ -717,7 +713,7 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept if (region == nullptr) return false; - if (triggerType == TriggerType::NoteOn && region->offBy == group) { + if (triggerEvent.type == TriggerEventType::NoteOn && region->offBy == group) { off(delay); return true; } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 0aec391e..59ed0a46 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include "TriggerEvent.h" #include "Config.h" #include "ADSREnvelope.h" #include "HistoricalBuffer.h" @@ -113,11 +114,9 @@ public: * * @param region * @param delay - * @param number - * @param value - * @param triggerType + * @param evebt */ - void startVoice(Region* region, int delay, int number, float value, TriggerType triggerType) noexcept; + void startVoice(Region* region, int delay, const TriggerEvent& event) noexcept; /** * @brief Get the sample quality determined by the active region. @@ -198,23 +197,11 @@ public: */ bool releasedOrFree() const noexcept; /** - * @brief Get the number that triggered the voice (note number or cc number) + * @brief Get the event that triggered the voice * * @return int */ - int getTriggerNumber() const noexcept { return triggerNumber; } - /** - * @brief Get the value that triggered the voice (note velocity or cc value) - * - * @return float - */ - float getTriggerValue() const noexcept { return triggerValue; } - /** - * @brief Get the type of trigger - * - * @return TriggerType - */ - TriggerType getTriggerType() const noexcept { return triggerType; } + const TriggerEvent& getTriggerEvent() const noexcept { return triggerEvent; } /** * @brief Reset the voice to its initial values @@ -432,9 +419,7 @@ private: State state { State::idle }; bool noteIsOff { false }; - TriggerType triggerType; - int triggerNumber; - float triggerValue; + TriggerEvent triggerEvent; absl::optional triggerDelay; float speedRatio { 1.0 }; @@ -496,13 +481,16 @@ inline bool sisterVoices(const Voice* lhs, const Voice* rhs) if (lhs->getAge() != rhs->getAge()) return false; - if (lhs->getTriggerNumber() != rhs->getTriggerNumber()) + const TriggerEvent& lhsTrigger = lhs->getTriggerEvent(); + const TriggerEvent& rhsTrigger = rhs->getTriggerEvent(); + + if (lhsTrigger.number != rhsTrigger.number) return false; - if (lhs->getTriggerValue() != rhs->getTriggerValue()) + if (lhsTrigger.value != rhsTrigger.value) return false; - if (lhs->getTriggerType() != rhs->getTriggerType()) + if (lhsTrigger.type != rhsTrigger.type) return false; return true; @@ -513,14 +501,17 @@ inline bool voiceOrdering(const Voice* lhs, const Voice* rhs) if (lhs->getAge() != rhs->getAge()) return lhs->getAge() > rhs->getAge(); - if (lhs->getTriggerNumber() != rhs->getTriggerNumber()) - return lhs->getTriggerNumber() < rhs->getTriggerNumber(); + const TriggerEvent& lhsTrigger = lhs->getTriggerEvent(); + const TriggerEvent& rhsTrigger = rhs->getTriggerEvent(); - if (lhs->getTriggerValue() != rhs->getTriggerValue()) - return lhs->getTriggerValue() < rhs->getTriggerValue(); + if (lhsTrigger.number != rhsTrigger.number) + return lhsTrigger.number < rhsTrigger.number; - if (lhs->getTriggerType() != rhs->getTriggerType()) - return lhs->getTriggerType() > rhs->getTriggerType(); + if (lhsTrigger.value != rhsTrigger.value) + return lhsTrigger.value < rhsTrigger.value; + + if (lhsTrigger.type != rhsTrigger.type) + return lhsTrigger.type > rhsTrigger.type; return false; } diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index b45b2e4c..1610d7b3 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -227,11 +227,11 @@ TEST_CASE("[Polyphony] Self-masking") REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } @@ -248,11 +248,11 @@ TEST_CASE("[Polyphony] Not self-masking") REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } @@ -269,11 +269,11 @@ TEST_CASE("[Polyphony] Self-masking with the exact same velocity") REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } @@ -286,9 +286,9 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high") synth.noteOn(0, 64, 63 ); synth.noteOn(0, 64, 62 ); REQUIRE( synth.getNumActiveVoices(true) == 2 ); // Both notes are playing - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } @@ -305,13 +305,13 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po REQUIRE( synth.getNumActiveVoices(true) == 4); synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 1 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); } @@ -333,9 +333,9 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 1 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } @@ -353,13 +353,13 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups") REQUIRE( synth.getNumActiveVoices(true) == 4); // Both notes are playing synth.renderBlock(buffer); REQUIRE(numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); - REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); } @@ -381,8 +381,8 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.renderBlock(buffer); REQUIRE(numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 78f017b6..3cbc68fe 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -660,7 +660,7 @@ TEST_CASE("[Synth] Apply function on sisters") REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 3 ); float start = 1.0f; sfz::SisterVoiceRing::applyToRing(synth.getVoiceView(0), [&](const sfz::Voice* v) { - start += static_cast(v->getTriggerNumber()); + start += static_cast(v->getTriggerEvent().number); }); REQUIRE( start == 1.0f + 3.0f * 63.0f ); } @@ -828,7 +828,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") std::vector requiredVelocities { 34_norm, 78_norm, 85_norm}; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); @@ -856,7 +856,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); @@ -886,7 +886,7 @@ TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); @@ -914,7 +914,7 @@ TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") std::vector requiredVelocities { 78_norm, 85_norm, 78_norm, 85_norm }; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); From 944373ea83749d4580f58e6fab500ca24e625286 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 21:34:42 +0200 Subject: [PATCH 193/445] Move common voice starting logic in a separate method --- src/sfizz/Synth.cpp | 61 ++++++++++++++++----------------------------- src/sfizz/Synth.h | 1 + 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 850456b2..890f990b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -893,6 +893,24 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept noteOffDispatch(delay, noteNumber, replacedVelocity); } +void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept +{ + checkNotePolyphony(region, delay, triggerEvent); + checkRegionPolyphony(region, delay); + checkGroupPolyphony(region, delay); + checkSetPolyphony(region, delay); + + Voice* selectedVoice = findFreeVoice(); + if (selectedVoice == nullptr) + return; + + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, triggerEvent); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); +} + bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) { const sfz::TriggerEvent& event = voice.getTriggerEvent(); @@ -926,13 +944,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex continue; } - if (Voice* selectedVoice = findFreeVoice()) { - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, triggerEvent); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); - } + startVoice(region, delay, triggerEvent, ring); } } } @@ -1040,18 +1052,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } } - checkNotePolyphony(region, delay, triggerEvent); - checkRegionPolyphony(region, delay); - checkGroupPolyphony(region, delay); - checkSetPolyphony(region, delay); - - if (Voice* selectedVoice = findFreeVoice()) { - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, triggerEvent); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); - } + startVoice(region, delay, triggerEvent, ring); } } } @@ -1096,10 +1097,6 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept if (ccNumber == region->sustainCC) { if (!region->rtDead) { // check that a voice with compatible trigger is playing - // FIXME: we're going twice over the voices, when the synth - // handles the regions completely these dispatch functions - // should be overhauled, also to include voice stealing on - // all events const auto compatibleVoice = [region](const VoicePtr& v) -> bool { return matchReleaseRegionAndVoice(*region, *v); }; @@ -1110,29 +1107,15 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept for (auto& note: region->delayedReleases) { // FIXME: we really need to have some form of common method to find and start voices... - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; - voice->startVoice(region, delay, noteOffEvent); - - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + startVoice(region, delay, noteOffEvent, ring); } region->delayedReleases.clear(); } if (region->registerCC(ccNumber, normValue)) { - if (Voice* selectedVoice = findFreeVoice()) { - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, triggerEvent); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); - } + startVoice(region, delay, triggerEvent, ring); } } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 1d2f1f5f..73403913 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -784,6 +784,7 @@ private: void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept; void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; + void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; std::array noteActivationLists; std::array ccActivationLists; From 01c43f03d4a828d6639448d5732bc5f4d6441051 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 21:39:33 +0200 Subject: [PATCH 194/445] Move the delayed release check in a separate method --- src/sfizz/Synth.cpp | 48 +++++++++++++++++++++++---------------------- src/sfizz/Synth.h | 1 + 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 890f990b..6b558dbb 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1044,7 +1044,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - for (auto& voice : voices) { if (voice->checkOffGroup(delay, region->group)) { const TriggerEvent& event = voice->getTriggerEvent(); @@ -1057,6 +1056,28 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } } +void sfz::Synth::checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +{ + if (!region->rtDead) { + // check that a voice with compatible trigger is playing + const auto compatibleVoice = [region](const VoicePtr& v) -> bool { + return matchReleaseRegionAndVoice(*region, *v); + }; + + if (absl::c_find_if(voices, compatibleVoice) == voices.end()) + region->delayedReleases.clear(); + } + + for (auto& note: region->delayedReleases) { + // FIXME: we really need to have some form of common method to find and start voices... + const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; + startVoice(region, delay, noteOffEvent, ring); + } + + region->delayedReleases.clear(); +} + + void sfz::Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept { const auto normalizedCC = normalizeCC(ccValue); @@ -1092,31 +1113,12 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept SisterVoiceRingBuilder ring; const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, normValue }; - for (auto& region : ccActivationLists[ccNumber]) { - if (ccNumber == region->sustainCC) { - if (!region->rtDead) { - // check that a voice with compatible trigger is playing - const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return matchReleaseRegionAndVoice(*region, *v); - }; + if (ccNumber == region->sustainCC) + checkDelayedReleases(region, delay, ring); - if (absl::c_find_if(voices, compatibleVoice) == voices.end()) - region->delayedReleases.clear(); - } - - for (auto& note: region->delayedReleases) { - // FIXME: we really need to have some form of common method to find and start voices... - const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; - startVoice(region, delay, noteOffEvent, ring); - } - - region->delayedReleases.clear(); - } - - if (region->registerCC(ccNumber, normValue)) { + if (region->registerCC(ccNumber, normValue)) startVoice(region, delay, triggerEvent, ring); - } } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 73403913..d7d9543a 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -785,6 +785,7 @@ private: void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; + void checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; std::array noteActivationLists; std::array ccActivationLists; From e32348190f80e74313e49d7cf1e3c0c72405e2a0 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 21:59:55 +0200 Subject: [PATCH 195/445] Modularize the cc dispatch and the matching between a playing voice and release regions --- src/sfizz/Synth.cpp | 93 ++++++++++++++++++++++----------------------- src/sfizz/Synth.h | 5 ++- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 6b558dbb..3226d447 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -911,15 +911,22 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg polyphonyGroups[region->group].registerVoice(selectedVoice); } -bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) +bool sfz::Synth::matchAttackRegion(const Region* releaseRegion) noexcept { - const sfz::TriggerEvent& event = voice.getTriggerEvent(); - return ( - !voice.isFree() - && event.type == sfz::TriggerEventType::NoteOn - && region.keyRange.containsWithEnd(event.number) - && region.velocityRange.containsWithEnd(event.value) - ); + const auto compatibleVoice = [releaseRegion](const Voice* v) -> bool { + const sfz::TriggerEvent& event = v->getTriggerEvent(); + return ( + !v->isFree() + && event.type == sfz::TriggerEventType::NoteOn + && releaseRegion->keyRange.containsWithEnd(event.number) + && releaseRegion->velocityRange.containsWithEnd(event.value) + ); + }; + + if (absl::c_find_if(voiceViewArray, compatibleVoice) == voiceViewArray.end()) + return false; + else + return true; } void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept @@ -930,19 +937,8 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { - if (region->triggerOnNote && region->trigger == SfzTrigger::release && !region->rtDead) { - // check that a voice with compatible trigger is playing - // FIXME: we're going twice over the voices, when the synth - // handles the regions completely these dispatch functions - // should be overhauled, also to include voice stealing on - // all events - const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return matchReleaseRegionAndVoice(*region, *v); - }; - - if (absl::c_find_if(voices, compatibleVoice) == voices.end()) - continue; - } + if (region->trigger == SfzTrigger::release && !region->rtDead && !matchAttackRegion(region)) + continue; startVoice(region, delay, triggerEvent, ring); } @@ -1036,6 +1032,16 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept } } +void sfz::Synth::checkOffGroups(Region* region, int delay) noexcept +{ + for (auto& voice : voices) { + if (voice->checkOffGroup(delay, region->group)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } + } +} + void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); @@ -1044,28 +1050,17 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - for (auto& voice : voices) { - if (voice->checkOffGroup(delay, region->group)) { - const TriggerEvent& event = voice->getTriggerEvent(); - noteOffDispatch(delay, event.number, event.value); - } - } - + checkOffGroups(region, delay); startVoice(region, delay, triggerEvent, ring); } } } -void sfz::Synth::checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +void sfz::Synth::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept { - if (!region->rtDead) { - // check that a voice with compatible trigger is playing - const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return matchReleaseRegionAndVoice(*region, *v); - }; - - if (absl::c_find_if(voices, compatibleVoice) == voices.end()) - region->delayedReleases.clear(); + if (!region->rtDead && !matchAttackRegion(region)) { + region->delayedReleases.clear(); + return; } for (auto& note: region->delayedReleases) { @@ -1073,7 +1068,6 @@ void sfz::Synth::checkDelayedReleases(Region* region, int delay, SisterVoiceRing const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; startVoice(region, delay, noteOffEvent, ring); } - region->delayedReleases.clear(); } @@ -1084,6 +1078,19 @@ void sfz::Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept hdcc(delay, ccNumber, normalizedCC); } +void sfz::Synth::ccDispatch(int delay, int ccNumber, float value) noexcept +{ + SisterVoiceRingBuilder ring; + const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value }; + for (auto& region : ccActivationLists[ccNumber]) { + if (ccNumber == region->sustainCC) + startDelayedReleaseVoices(region, delay, ring); + + if (region->registerCC(ccNumber, value)) + startVoice(region, delay, triggerEvent, ring); + } +} + void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept { ASSERT(ccNumber < config::numCCs); @@ -1111,15 +1118,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept for (auto& voice : voices) voice->registerCC(delay, ccNumber, normValue); - SisterVoiceRingBuilder ring; - const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, normValue }; - for (auto& region : ccActivationLists[ccNumber]) { - if (ccNumber == region->sustainCC) - checkDelayedReleases(region, delay, ring); - - if (region->registerCC(ccNumber, normValue)) - startVoice(region, delay, triggerEvent, ring); - } + ccDispatch(delay, ccNumber, normValue); } void sfz::Synth::pitchWheel(int delay, int pitch) noexcept diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index d7d9543a..3a12c5bb 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -727,6 +727,7 @@ private: void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; + void ccDispatch(int delay, int ccNumber, float value) noexcept; template static void updateUsedCCsFromCCMap(std::bitset& usedCCs, const CCMap map) @@ -785,7 +786,9 @@ private: void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; - void checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; + void checkOffGroups(Region* region, int delay) noexcept; + void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; + bool matchAttackRegion(const Region* region) noexcept; std::array noteActivationLists; std::array ccActivationLists; From 025e87a0d1f22df3dc7ecef3d7119f9b3f6ba788 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 22:01:44 +0200 Subject: [PATCH 196/445] Remove the Voice::TriggerType enum --- src/sfizz/Voice.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 59ed0a46..682132ae 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -44,12 +44,6 @@ public: ~Voice(); - enum class TriggerType { - NoteOn, - NoteOff, - CC - }; - /** * @brief Get the unique identifier of this voice in a synth */ From 6750fb93a2ce8d14a0fc21d0a9417f7289840010 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 22:09:32 +0200 Subject: [PATCH 197/445] Comments and renaming --- src/sfizz/Synth.cpp | 6 ++-- src/sfizz/Synth.h | 86 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 3226d447..f32b47fa 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -911,7 +911,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg polyphonyGroups[region->group].registerVoice(selectedVoice); } -bool sfz::Synth::matchAttackRegion(const Region* releaseRegion) noexcept +bool sfz::Synth::playingAttackVoice(const Region* releaseRegion) noexcept { const auto compatibleVoice = [releaseRegion](const Voice* v) -> bool { const sfz::TriggerEvent& event = v->getTriggerEvent(); @@ -937,7 +937,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { - if (region->trigger == SfzTrigger::release && !region->rtDead && !matchAttackRegion(region)) + if (region->trigger == SfzTrigger::release && !region->rtDead && !playingAttackVoice(region)) continue; startVoice(region, delay, triggerEvent, ring); @@ -1058,7 +1058,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc void sfz::Synth::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept { - if (!region->rtDead && !matchAttackRegion(region)) { + if (!region->rtDead && !playingAttackVoice(region)) { region->delayedReleases.clear(); return; } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3a12c5bb..6c22fdeb 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -725,8 +725,31 @@ private: fs::file_time_type checkModificationTime(); + /** + * @brief Check all regions and start voices for note on events + * + * @param delay + * @param noteNumber + * @param velocity + */ void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; + + /** + * @brief Check all regions and start voices for note off events + * + * @param delay + * @param noteNumber + * @param velocity + */ void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; + + /** + * @brief Check all regions and start voices for cc events + * + * @param delay + * @param ccNumber + * @param value + */ void ccDispatch(int delay, int ccNumber, float value) noexcept; template @@ -781,14 +804,75 @@ private: VoiceViewVector voiceViewArray; VoiceStealing stealer; + /** + * @brief Check the region polyphony, releasing voices if necessary + * + * @param region + * @param delay + */ void checkRegionPolyphony(const Region* region, int delay) noexcept; + + /** + * @brief Check the note polyphony, releasing voices if necessary + * + * @param region + * @param delay + * @param triggerEvent + */ void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept; + + /** + * @brief Check the group polyphony, releasing voices if necessary + * + * @param region + * @param delay + */ void checkGroupPolyphony(const Region* region, int delay) noexcept; + + /** + * @brief Check the region set polyphony at all levels, releasing voices if necessary + * + * @param region + * @param delay + */ void checkSetPolyphony(const Region* region, int delay) noexcept; + + /** + * @brief Start a voice for a specific region. + * This will do the needed polyphony checks and voice stealing. + * + * @param region + * @param delay + * @param triggerEvent + * @param ring + */ void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; + + /** + * @brief Check the off groups of all playing voices, releasing if necessary + * + * @param region + * @param delay + */ void checkOffGroups(Region* region, int delay) noexcept; + + /** + * @brief Start all delayed release voices of the region if necessary + * + * @param region + * @param delay + * @param ring + */ void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; - bool matchAttackRegion(const Region* region) noexcept; + + /** + * @brief Check if a playing voice matches the release region + * + * @param releaseRegion + * @return true + * @return false + */ + bool playingAttackVoice(const Region* releaseRegion) noexcept; std::array noteActivationLists; std::array ccActivationLists; From 1f65c135bcdb6f4f938105199337c17cabc7d98f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 23:23:14 +0200 Subject: [PATCH 198/445] Don't release age 0 voices --- src/sfizz/Synth.cpp | 8 +++++++- src/sfizz/VoiceStealing.cpp | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f32b47fa..d141eda1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -665,6 +665,11 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (stolenVoice == nullptr) return {}; + // Never kill age 0 voices + if (stolenVoice->getAge() == 0) + return {}; + + auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); SisterVoiceRing::applyToRing(stolenVoice, [&] (Voice* v) { renderVoiceToOutputs(*v, *tempSpan); @@ -991,8 +996,9 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const Trigg } } - if (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) + if (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) { SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); + } } void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 0fa4f1a7..7ea7c867 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -25,7 +25,7 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept // their sound, but it's reasonable for sounds with a quick attack and longer // release. const auto ageThreshold = - static_cast(voices.front()->getAge() * config::stealingAgeCoeff) + 1; + static_cast(voices.front()->getAge() * config::stealingAgeCoeff); Voice* returnedVoice = voices.front(); unsigned idx = 0; From fa37669e083c2669d57cae8c505fc17c824848fa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 26 Aug 2020 01:04:10 +0200 Subject: [PATCH 199/445] Add checks for release voices and note_polyphony --- src/sfizz/Synth.cpp | 6 ++++-- src/sfizz/Voice.cpp | 2 +- tests/PolyphonyT.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index d141eda1..64505a62 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -976,7 +976,8 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const Trigg for (Voice* voice : voiceViewArray) { const sfz::TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent(); - if (!voice->releasedOrFree() + const bool skipVoice = (triggerEvent.type == TriggerEventType::NoteOn && voice->releasedOrFree()) || voice->isFree(); + if (!skipVoice && voice->getRegion()->group == region->group && voiceTriggerEvent.number == triggerEvent.number && voiceTriggerEvent.type == triggerEvent.type) { @@ -984,8 +985,9 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const Trigg switch (region->selfMask) { case SfzSelfMask::mask: if (voiceTriggerEvent.value <= triggerEvent.value) { - if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) + if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) { selfMaskCandidate = voice; + } } break; case SfzSelfMask::dontMask: diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 9687ebc4..117af317 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -193,7 +193,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex if (state != State::playing) return; - if (triggerEvent.number == noteNumber) { + if (triggerEvent.number == noteNumber && triggerEvent.type == TriggerEventType::NoteOn) { noteIsOff = true; if (region->loopMode == SfzLoopMode::one_shot) diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 1610d7b3..1ff525cf 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -286,6 +286,7 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high") synth.noteOn(0, 64, 63 ); synth.noteOn(0, 64, 62 ); REQUIRE( synth.getNumActiveVoices(true) == 2 ); // Both notes are playing + REQUIRE( numPlayingVoices(synth) == 2 ); // id REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); @@ -386,3 +387,45 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } + +TEST_CASE("[Polyphony] Note polyphony operates on release voices") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + 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 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); + synth.noteOn(20, 48, 65 ); + synth.noteOff(30, 48, 10 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 65_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + +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 buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + 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 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(20, 48, 61 ); + synth.noteOff(30, 48, 10 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 61_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} From bb621282c9b13e4316fa4a2514a49b0fc23f6be6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 26 Aug 2020 01:27:57 +0200 Subject: [PATCH 200/445] Note polyphony tests and bugs --- src/sfizz/SisterVoiceRing.h | 11 ++---- tests/PolyphonyT.cpp | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 93dbc515..4e7cc743 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -128,14 +128,6 @@ struct SisterVoiceRing { */ class SisterVoiceRingBuilder { public: - ~SisterVoiceRingBuilder() noexcept { - if (lastStartedVoice != nullptr) { - ASSERT(firstStartedVoice); - lastStartedVoice->setNextSisterVoice(firstStartedVoice); - firstStartedVoice->setPreviousSisterVoice(lastStartedVoice); - } - } - /** * @brief Add a voice to the sister ring * @@ -145,6 +137,9 @@ public: if (firstStartedVoice == nullptr) firstStartedVoice = voice; + firstStartedVoice->setPreviousSisterVoice(voice); + voice->setNextSisterVoice(firstStartedVoice); + if (lastStartedVoice != nullptr) { voice->setPreviousSisterVoice(lastStartedVoice); lastStartedVoice->setNextSisterVoice(voice); diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 1ff525cf..b7de5c7a 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -429,3 +429,71 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 61_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } + +TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + key=48 sample=*silence + key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1 + )"); + synth.cc(0, 64, 127); + synth.noteOn(0, 48, 61 ); + synth.noteOff(1, 48, 0 ); + synth.noteOn(2, 48, 62 ); + synth.noteOff(3, 48, 0 ); + synth.noteOn(4, 48, 63 ); + synth.noteOff(5, 48, 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 3); + REQUIRE( numPlayingVoices(synth) == 3 ); + synth.cc(20, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + 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()); +} + +TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal (masking)") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + key=48 sample=*silence + key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1 + )"); + synth.cc(0, 64, 127); + synth.noteOn(0, 48, 63 ); + synth.noteOff(1, 48, 0 ); + synth.noteOn(2, 48, 62 ); + synth.noteOff(3, 48, 0 ); + synth.noteOn(4, 48, 61 ); + synth.noteOff(5, 48, 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 3); + REQUIRE( numPlayingVoices(synth) == 3 ); + synth.cc(20, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + 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()); +} From 863a08c421d563b11ae425a3ece59f89b8dc1c30 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 7 Sep 2020 23:18:02 +0200 Subject: [PATCH 201/445] Use Cakewalk/RGC behavior for self-choking notes Basically if group=off_by, a note will not choke other voices with the same note number --- src/sfizz/Synth.cpp | 18 +++++++----------- src/sfizz/Synth.h | 8 -------- src/sfizz/Voice.cpp | 8 +++++--- src/sfizz/Voice.h | 3 ++- tests/SynthT.cpp | 23 +++++++++++++++++++---- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 64505a62..b5f7298d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1040,16 +1040,6 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept } } -void sfz::Synth::checkOffGroups(Region* region, int delay) noexcept -{ - for (auto& voice : voices) { - if (voice->checkOffGroup(delay, region->group)) { - const TriggerEvent& event = voice->getTriggerEvent(); - noteOffDispatch(delay, event.number, event.value); - } - } -} - void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); @@ -1058,7 +1048,13 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - checkOffGroups(region, delay); + for (auto& voice : voices) { + if (voice->checkOffGroup(region, delay, noteNumber)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } + } + startVoice(region, delay, triggerEvent, ring); } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 6c22fdeb..995bb5e6 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -848,14 +848,6 @@ private: */ void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; - /** - * @brief Check the off groups of all playing voices, releasing if necessary - * - * @param region - * @param delay - */ - void checkOffGroups(Region* region, int delay) noexcept; - /** * @brief Start all delayed release voices of the region if necessary * diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 117af317..4f120348 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -708,12 +708,14 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept #endif } -bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept +bool sfz::Voice::checkOffGroup(const Region* other, int delay, int noteNumber) noexcept { - if (region == nullptr) + if (region == nullptr || other == nullptr) return false; - if (triggerEvent.type == TriggerEventType::NoteOn && region->offBy == group) { + if (triggerEvent.type == TriggerEventType::NoteOn + && region->offBy == other->group + && noteNumber != triggerEvent.number) { off(delay); return true; } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 682132ae..a010aaa8 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -163,11 +163,12 @@ public: * This will trigger the release if true. * * @param delay + * @param noteNumber * @param group * @return true * @return false */ - bool checkOffGroup(int delay, uint32_t group) noexcept; + bool checkOffGroup(const Region* other, int delay, int noteNumber) noexcept; /** * @brief Render a block of data for this voice into the span diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 3cbc68fe..69f722ae 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1256,8 +1256,24 @@ TEST_CASE("[Synth] Off by same group") REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(60) ); } +TEST_CASE("[Synth] Off by alone and repeated") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; -TEST_CASE("[Synth] Off by same note") + synth.loadSfzString(fs::current_path(), R"( + group=1 off_by=1 sample=*sine key=60 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 3 ); +} + + +TEST_CASE("[Synth] Off by same note and group") { sfz::Synth synth; sfz::AudioBuffer buffer { 2, 256 }; @@ -1267,7 +1283,6 @@ TEST_CASE("[Synth] Off by same note") group=1 off_by=1 sample=*triangle key=60 )"); synth.noteOn(0, 60, 85); - REQUIRE( numPlayingVoices(synth) == 1 ); - auto playingVoices = getPlayingVoices(synth); - REQUIRE( playingVoices.front()->getRegion()->sampleId.filename() == "*triangle" ); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.noteOn(0, 60, 85); } From bc55705290b2523e626973166dddb086942f2cc8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 7 Sep 2020 23:47:28 +0200 Subject: [PATCH 202/445] Put default quality to 1 --- src/sfizz/Defaults.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 302d86b4..d53f3772 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -245,7 +245,7 @@ namespace Default constexpr Range egOnCCPercentRange { -100.0, 100.0 }; // ***** SFZ v2 ******** - constexpr int sampleQuality { 2 }; + constexpr int sampleQuality { 1 }; constexpr int sampleQualityInFreewheelingMode { 10 }; // for future use, possibly excessive constexpr Range sampleQualityRange { 1, 10 }; // sample_quality From 0e7ba04c8cca53c35723a966266d02a5f296716c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 8 Sep 2020 08:25:07 +0200 Subject: [PATCH 203/445] Don't search these libs, already done in SfizzConfig --- editor/cmake/Vstgui.cmake | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 25f185bb..06031d80 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -136,16 +136,6 @@ if(WIN32) "${SHLWAPI_LIBRARY}") endif() elseif(APPLE) - find_library(APPLE_COREFOUNDATION_LIBRARY "CoreFoundation") - find_library(APPLE_FOUNDATION_LIBRARY "Foundation") - find_library(APPLE_COCOA_LIBRARY "Cocoa") - find_library(APPLE_OPENGL_LIBRARY "OpenGL") - find_library(APPLE_ACCELERATE_LIBRARY "Accelerate") - find_library(APPLE_QUARTZCORE_LIBRARY "QuartzCore") - find_library(APPLE_CARBON_LIBRARY "Carbon") - find_library(APPLE_AUDIOTOOLBOX_LIBRARY "AudioToolbox") - find_library(APPLE_COREAUDIO_LIBRARY "CoreAudio") - find_library(APPLE_COREMIDI_LIBRARY "CoreMIDI") target_link_libraries(sfizz-vstgui PRIVATE "${APPLE_COREFOUNDATION_LIBRARY}" "${APPLE_FOUNDATION_LIBRARY}" From d2362609a0ee574812b0567ce0bfae7f046c0628 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 8 Sep 2020 08:47:53 +0200 Subject: [PATCH 204/445] Prevent pitchbend events from creating xruns --- src/sfizz/MidiState.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 3cb0a9ff..03e65a9e 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -76,6 +76,8 @@ void sfz::MidiState::setSamplesPerBlock(int samplesPerBlock) noexcept ccEvents.shrink_to_fit(); ccEvents.reserve(samplesPerBlock); } + pitchEvents.shrink_to_fit(); + pitchEvents.reserve(samplesPerBlock); } float sfz::MidiState::getNoteDuration(int noteNumber, int delay) const From db194be867e8b63a88e4f9cf5ab9bddc12cd9b29 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 8 Sep 2020 09:00:18 +0200 Subject: [PATCH 205/445] Initialize SIMD in both synth ctors --- src/sfizz/Synth.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 5a0b6103..4fc9b0f1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -30,11 +30,12 @@ sfz::Synth::Synth() : Synth(config::numVoices) { - initializeSIMDDispatchers(); } sfz::Synth::Synth(int numVoices) { + initializeSIMDDispatchers(); + const std::lock_guard disableCallback { callbackGuard }; parser.setListener(this); effectFactory.registerStandardEffectTypes(); From 22585e02a1e084090f68ad215b2582cb2076d236 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 8 Sep 2020 11:32:38 +0200 Subject: [PATCH 206/445] Forbid functions to forget the return statement --- cmake/SfizzConfig.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 270954e8..b753efd9 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -43,6 +43,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Wall) add_compile_options(-Wextra) add_compile_options(-fno-omit-frame-pointer) # For debugging purposes + add_compile_options(-Werror=return-type) if (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(i.86|x86_64)$") add_compile_options(-msse2) endif() From 36d0c54e0030cc3590ddb837ddda9a73afe219ec Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 8 Sep 2020 13:20:49 +0200 Subject: [PATCH 207/445] Add a switch for the render quality --- clients/sfizz_render.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clients/sfizz_render.cpp b/clients/sfizz_render.cpp index 4d2a3fd4..81c2966d 100644 --- a/clients/sfizz_render.cpp +++ b/clients/sfizz_render.cpp @@ -65,6 +65,7 @@ int main(int argc, char** argv) bool verbose { false }; bool help { false }; bool useEOT { false }; + int quality { 2 }; int oversampling { 1 }; options.add_options() @@ -74,6 +75,7 @@ int main(int argc, char** argv) ("b,blocksize", "Block size for the sfizz callbacks", cxxopts::value(blockSize)) ("s,samplerate", "Output sample rate", cxxopts::value(sampleRate)) ("oversampling", "Internal oversampling factor", cxxopts::value(oversampling)) + ("q,quality", "Resampling quality", cxxopts::value(quality)) ("v,verbose", "Verbose output", cxxopts::value(verbose)) ("log", "Produce logs", cxxopts::value()) ("use-eot", "End the rendering at the last End of Track Midi message", cxxopts::value(useEOT)) @@ -118,6 +120,7 @@ int main(int argc, char** argv) sfz::Synth synth; synth.setSamplesPerBlock(blockSize); synth.setSampleRate(sampleRate); + synth.setSampleQuality(sfz::Synth::ProcessMode::ProcessFreewheeling, 1); synth.enableFreeWheeling(); if (params.count("log") > 0) From 787fdd26ae72b1f877d28d2d57fa713d2b9654ed Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 8 Sep 2020 14:11:15 +0200 Subject: [PATCH 208/445] Use the command line switch Stupid me --- clients/sfizz_render.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/sfizz_render.cpp b/clients/sfizz_render.cpp index 81c2966d..bd03ea20 100644 --- a/clients/sfizz_render.cpp +++ b/clients/sfizz_render.cpp @@ -120,7 +120,7 @@ int main(int argc, char** argv) sfz::Synth synth; synth.setSamplesPerBlock(blockSize); synth.setSampleRate(sampleRate); - synth.setSampleQuality(sfz::Synth::ProcessMode::ProcessFreewheeling, 1); + synth.setSampleQuality(sfz::Synth::ProcessMode::ProcessFreewheeling, quality); synth.enableFreeWheeling(); if (params.count("log") > 0) From 5bb28128f8d9c73f484ef3988dbf4b680673c285 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 7 Sep 2020 05:52:18 -0700 Subject: [PATCH 209/445] Allow the AudioUnit to build again --- cmake/SfizzConfig.cmake | 15 +++++++++++++++ vst/CMakeLists.txt | 1 + 2 files changed, 16 insertions(+) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index b753efd9..02c38fc3 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -27,6 +27,21 @@ if (WIN32) add_compile_definitions(NOMINMAX) endif() +# Find macOS system libraries +if(APPLE) + find_library(APPLE_COREFOUNDATION_LIBRARY "CoreFoundation") + find_library(APPLE_FOUNDATION_LIBRARY "Foundation") + find_library(APPLE_COCOA_LIBRARY "Cocoa") + find_library(APPLE_CARBON_LIBRARY "Carbon") + find_library(APPLE_OPENGL_LIBRARY "OpenGL") + find_library(APPLE_ACCELERATE_LIBRARY "Accelerate") + find_library(APPLE_QUARTZCORE_LIBRARY "QuartzCore") + find_library(APPLE_AUDIOTOOLBOX_LIBRARY "AudioToolbox") + find_library(APPLE_AUDIOUNIT_LIBRARY "AudioUnit") + find_library(APPLE_COREAUDIO_LIBRARY "CoreAudio") + find_library(APPLE_COREMIDI_LIBRARY "CoreMIDI") +endif() + # The variable CMAKE_SYSTEM_PROCESSOR is incorrect on Visual studio... # see https://gitlab.kitware.com/cmake/cmake/issues/15170 diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index c4b3538e..e93cb989 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -145,6 +145,7 @@ elseif(SFIZZ_AU) "${APPLE_COCOA_LIBRARY}" "${APPLE_CARBON_LIBRARY}" "${APPLE_AUDIOTOOLBOX_LIBRARY}" + "${APPLE_AUDIOUNIT_LIBRARY}" "${APPLE_COREAUDIO_LIBRARY}" "${APPLE_COREMIDI_LIBRARY}") From 9a417826c2278a93e2be199895973d069125babd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 8 Sep 2020 08:25:07 +0200 Subject: [PATCH 210/445] Don't search these libs, already done in SfizzConfig --- editor/cmake/Vstgui.cmake | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 2cac1f17..e21dbbd5 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -136,16 +136,6 @@ if(WIN32) "${SHLWAPI_LIBRARY}") endif() elseif(APPLE) - find_library(APPLE_COREFOUNDATION_LIBRARY "CoreFoundation") - find_library(APPLE_FOUNDATION_LIBRARY "Foundation") - find_library(APPLE_COCOA_LIBRARY "Cocoa") - find_library(APPLE_OPENGL_LIBRARY "OpenGL") - find_library(APPLE_ACCELERATE_LIBRARY "Accelerate") - find_library(APPLE_QUARTZCORE_LIBRARY "QuartzCore") - find_library(APPLE_CARBON_LIBRARY "Carbon") - find_library(APPLE_AUDIOTOOLBOX_LIBRARY "AudioToolbox") - find_library(APPLE_COREAUDIO_LIBRARY "CoreAudio") - find_library(APPLE_COREMIDI_LIBRARY "CoreMIDI") target_link_libraries(sfizz-vstgui PRIVATE "${APPLE_COREFOUNDATION_LIBRARY}" "${APPLE_FOUNDATION_LIBRARY}" From 7ec5da9ddbc062bed40efd7058ef6916b9ffcbbc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 8 Sep 2020 12:38:02 +0200 Subject: [PATCH 211/445] Memorize the flags to avoid a recurrent switch --- src/sfizz/modulations/ModKey.cpp | 5 ----- src/sfizz/modulations/ModKey.h | 7 +++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 5ba5e02c..bfdd198a 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -5,7 +5,6 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "ModKey.h" -#include "ModId.h" #include "../Debug.h" #include #include @@ -74,10 +73,6 @@ bool ModKey::isTarget() const noexcept return ModIds::isTarget(id_); } -int ModKey::flags() const noexcept -{ - return ModIds::flags(id_); -} std::string ModKey::toString() const { diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 37b8b072..96ab3705 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -6,6 +6,7 @@ #pragma once #include "ModKeyHash.h" +#include "ModId.h" #include "../NumericId.h" #include @@ -24,7 +25,7 @@ public: ModKey() = default; explicit ModKey(ModId id, NumericId region = {}, Parameters params = {}) - : id_(id), region_(region), params_(params) {} + : id_(id), region_(region), params_(params), flags_(ModIds::flags(id_)) {} static ModKey createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step); static ModKey createNXYZ(ModId id, NumericId region, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0); @@ -34,10 +35,10 @@ public: const ModId& id() const noexcept { return id_; } NumericId region() const noexcept { return region_; } const Parameters& parameters() const noexcept { return params_; } + int flags() const noexcept { return flags_; } bool isSource() const noexcept; bool isTarget() const noexcept; - int flags() const noexcept; std::string toString() const; struct Parameters { @@ -73,6 +74,8 @@ private: NumericId region_; //! List of values which identify the key uniquely, along with the hash and region Parameters params_ {}; + // Memorize the flag + int flags_; }; } // namespace sfz From db9840c5bd7064974563e55478723e2f5ad7ccd8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 8 Sep 2020 21:43:14 +0200 Subject: [PATCH 212/445] Put operator == in the constructor for the modkeyThis function is called alot, this seems to favor inlining --- src/sfizz/modulations/ModKey.cpp | 21 --------------------- src/sfizz/modulations/ModKey.h | 25 +++++++++++++++++++++---- src/sfizz/modulations/ModMatrix.cpp | 3 +-- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index bfdd198a..c5ee2a37 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -7,7 +7,6 @@ #include "ModKey.h" #include "../Debug.h" #include -#include namespace sfz { @@ -31,16 +30,6 @@ ModKey::Parameters& ModKey::Parameters::operator=(const Parameters& other) noexc return *this; } -bool ModKey::Parameters::operator==(const Parameters& other) const noexcept -{ - return std::memcmp(this, &other, sizeof(*this)) == 0; -} - -bool ModKey::Parameters::operator!=(const Parameters& other) const noexcept -{ - return std::memcmp(this, &other, sizeof(*this)) != 0; -} - ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step) { ModKey::Parameters p; @@ -106,13 +95,3 @@ std::string ModKey::toString() const } // namespace sfz -bool sfz::ModKey::operator==(const ModKey &other) const noexcept -{ - return id_ == other.id_ && region_ == other.region_ && - parameters() == other.parameters(); -} - -bool sfz::ModKey::operator!=(const ModKey &other) const noexcept -{ - return !this->operator==(other); -} diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 96ab3705..5fff0197 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -9,6 +9,7 @@ #include "ModId.h" #include "../NumericId.h" #include +#include namespace sfz { @@ -49,8 +50,15 @@ public: Parameters(Parameters&&) = delete; Parameters &operator=(Parameters&&) = delete; - bool operator==(const Parameters& other) const noexcept; - bool operator!=(const Parameters& other) const noexcept; + bool operator==(const Parameters& other) const noexcept + { + return std::memcmp(this, &other, sizeof(*this)) == 0; + } + + bool operator!=(const Parameters& other) const noexcept + { + return std::memcmp(this, &other, sizeof(*this)) != 0; + } union { //! Parameters if this key identifies a CC source @@ -64,8 +72,17 @@ public: }; public: - bool operator==(const ModKey &other) const noexcept; - bool operator!=(const ModKey &other) const noexcept; + bool operator==(const ModKey &other) const noexcept + { + return id_ == other.id_ && region_ == other.region_ && + parameters() == other.parameters(); + } + + bool operator!=(const ModKey &other) const noexcept + { + return !this->operator==(other); + } + private: //! Identifier diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 9290ba2d..643f168d 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -364,8 +364,7 @@ float* ModMatrix::getModulation(TargetId targetId) } else { ASSERT(targetFlags & kModIsAdditive); - for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] += sourceDepth * sourceBuffer[i]; + sfz::multiplyAdd1(sourceDepth, sourceBuffer, buffer); } } } From d4586a699c7851012e5231e514e40a08fd415ea7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 8 Sep 2020 22:25:31 +0200 Subject: [PATCH 213/445] Update GOVERNANCE.md --- GOVERNANCE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 3fc7d31c..e6eb0dae 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -47,4 +47,10 @@ It is the role of maintainers to best adapt the governance model to the evolutio 3. Maintainers can be invited from regular contributors at any point in time; maintainers wishing to leave the governance of the project may inform the other maintainers. In both cases, updates to this document reflect the evolution of the governance team and rules. +## License for the GOVERNANCE.md file + +Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty this notice is preserved. +This file is offered as-is, without any warranty. + [Open-source Open Collective]: https://opencollective.com/sfztools From be77d4544b121017bf10fd08a9947368313a4960 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 8 Sep 2020 22:35:45 +0200 Subject: [PATCH 214/445] Update GOVERNANCE.md --- GOVERNANCE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e6eb0dae..d809b00c 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -50,7 +50,7 @@ In both cases, updates to this document reflect the evolution of the governance ## License for the GOVERNANCE.md file Copying and distribution of this file, with or without modification, -are permitted in any medium without royalty this notice is preserved. -This file is offered as-is, without any warranty. +are permitted in any medium without royalty provided that this notice +is preserved. This file is offered as-is, without any warranty. [Open-source Open Collective]: https://opencollective.com/sfztools From 48b8011354c0f71bff295d0aca4e6c55eab5f1bf Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 8 Sep 2020 23:00:19 +0200 Subject: [PATCH 215/445] Add SIMD helpers targeted for the mod matrix and plug them in On my machine now the previous code path was probably vectorized but this way we have the structure to handle platforms where it's not --- benchmarks/BM_multiplyMul.cpp | 83 ++++++++++++++++++++++++ benchmarks/BM_multiplyMulFixedGain.cpp | 88 ++++++++++++++++++++++++++ benchmarks/CMakeLists.txt | 2 + src/sfizz/SIMDHelpers.cpp | 20 ++++++ src/sfizz/SIMDHelpers.h | 52 +++++++++++++++ src/sfizz/modulations/ModMatrix.cpp | 8 +-- src/sfizz/simd/HelpersSSE.cpp | 43 +++++++++++++ src/sfizz/simd/HelpersSSE.h | 2 + src/sfizz/simd/HelpersScalar.h | 16 +++++ tests/SIMDHelpersT.cpp | 79 +++++++++++++++++++++++ 10 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 benchmarks/BM_multiplyMul.cpp create mode 100644 benchmarks/BM_multiplyMulFixedGain.cpp diff --git a/benchmarks/BM_multiplyMul.cpp b/benchmarks/BM_multiplyMul.cpp new file mode 100644 index 00000000..d002fba0 --- /dev/null +++ b/benchmarks/BM_multiplyMul.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SIMDHelpers.h" +#include +#include +#include +#include +#include +#include + +class MultiplyMul : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) { + std::random_device rd { }; + std::mt19937 gen { rd() }; + std::uniform_real_distribution dist { 0.1f, 1.0f }; + input = std::vector(state.range(0)); + output = std::vector(state.range(0)); + gain = std::vector(state.range(0)); + std::fill(output.begin(), output.end(), 2.0f ); + std::generate(gain.begin(), gain.end(), [&]() { return dist(gen); }); + std::generate(input.begin(), input.end(), [&]() { return dist(gen); }); + } + + void TearDown(const ::benchmark::State& /* state */) { + + } + + std::vector gain; + std::vector input; + std::vector output; +}; + +BENCHMARK_DEFINE_F(MultiplyMul, Straight)(benchmark::State& state) { + for (auto _ : state) + { + for (int i = 0; i < state.range(0); ++i) + output[i] *= gain[i] * input[i]; + } +} + +BENCHMARK_DEFINE_F(MultiplyMul, Scalar)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, false); + sfz::multiplyMul(gain, input, absl::MakeSpan(output)); + } +} + +BENCHMARK_DEFINE_F(MultiplyMul, SIMD)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, true); + sfz::multiplyMul(gain, input, absl::MakeSpan(output)); + } +} + +BENCHMARK_DEFINE_F(MultiplyMul, Scalar_Unaligned)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, false); + sfz::multiplyMul(absl::MakeSpan(gain).subspan(1), absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1)); + } +} + +BENCHMARK_DEFINE_F(MultiplyMul, SIMD_Unaligned)(benchmark::State& state) { + for (auto _ : state) + { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, true); + sfz::multiplyMul(absl::MakeSpan(gain).subspan(1), absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1)); + } +} + +BENCHMARK_REGISTER_F(MultiplyMul, Straight)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMul, Scalar)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMul, SIMD)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMul, Scalar_Unaligned)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMul, SIMD_Unaligned)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_MAIN(); diff --git a/benchmarks/BM_multiplyMulFixedGain.cpp b/benchmarks/BM_multiplyMulFixedGain.cpp new file mode 100644 index 00000000..56988bef --- /dev/null +++ b/benchmarks/BM_multiplyMulFixedGain.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SIMDHelpers.h" +#include +#include +#include +#include +#include +#include + +class MultiplyMulFixedGain : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) + { + std::random_device rd {}; + std::mt19937 gen { rd() }; + std::uniform_real_distribution dist { 0.1f, 1.0f }; + input = std::vector(state.range(0)); + output = std::vector(state.range(0)); + gain = dist(gen); + std::fill(output.begin(), output.end(), 2.0f); + std::generate(input.begin(), input.end(), [&]() { return dist(gen); }); + } + + void TearDown(const ::benchmark::State& /* state */) + { + } + + float gain = {}; + std::vector input; + std::vector output; +}; + +BENCHMARK_DEFINE_F(MultiplyMulFixedGain, Straight) +(benchmark::State& state) +{ + for (auto _ : state) { + for (int i = 0; i < state.range(0); ++i) + output[i] *= gain * input[i]; + } +} + +BENCHMARK_DEFINE_F(MultiplyMulFixedGain, Scalar) +(benchmark::State& state) +{ + for (auto _ : state) { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, false); + sfz::multiplyMul1(gain, input, absl::MakeSpan(output)); + } +} + +BENCHMARK_DEFINE_F(MultiplyMulFixedGain, SIMD) +(benchmark::State& state) +{ + for (auto _ : state) { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, true); + sfz::multiplyMul1(gain, input, absl::MakeSpan(output)); + } +} + +BENCHMARK_DEFINE_F(MultiplyMulFixedGain, Scalar_Unaligned) +(benchmark::State& state) +{ + for (auto _ : state) { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, false); + sfz::multiplyMul1(gain, absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1)); + } +} + +BENCHMARK_DEFINE_F(MultiplyMulFixedGain, SIMD_Unaligned) +(benchmark::State& state) +{ + for (auto _ : state) { + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, true); + sfz::multiplyMul1(gain, absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1)); + } +} + +BENCHMARK_REGISTER_F(MultiplyMulFixedGain, Straight)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMulFixedGain, Scalar)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMulFixedGain, SIMD)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMulFixedGain, Scalar_Unaligned)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(MultiplyMulFixedGain, SIMD_Unaligned)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_MAIN(); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index cba645f3..0ecd4494 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -49,6 +49,8 @@ target_link_libraries(bm_ADSR PRIVATE sfizz::sfizz) sfizz_add_benchmark(bm_add BM_add.cpp) sfizz_add_benchmark(bm_multiplyAdd BM_multiplyAdd.cpp) sfizz_add_benchmark(bm_multiplyAddFixedGain BM_multiplyAddFixedGain.cpp) +sfizz_add_benchmark(bm_multiplyMul BM_multiplyMul.cpp) +sfizz_add_benchmark(bm_multiplyMulFixedGain BM_multiplyMulFixedGain.cpp) sfizz_add_benchmark(bm_subtract BM_subtract.cpp) sfizz_add_benchmark(bm_copy BM_copy.cpp) sfizz_add_benchmark(bm_mean BM_mean.cpp) diff --git a/src/sfizz/SIMDHelpers.cpp b/src/sfizz/SIMDHelpers.cpp index c0287772..f34124a4 100644 --- a/src/sfizz/SIMDHelpers.cpp +++ b/src/sfizz/SIMDHelpers.cpp @@ -29,6 +29,8 @@ struct SIMDDispatch { decltype(÷Scalar) divide = ÷Scalar; decltype(&multiplyAddScalar) multiplyAdd = &multiplyAddScalar; decltype(&multiplyAdd1Scalar) multiplyAdd1 = &multiplyAdd1Scalar; + decltype(&multiplyMulScalar) multiplyMul = &multiplyMulScalar; + decltype(&multiplyMul1Scalar) multiplyMul1 = &multiplyMul1Scalar; decltype(&linearRampScalar) linearRamp = &linearRampScalar; decltype(&multiplicativeRampScalar) multiplicativeRamp = &multiplicativeRampScalar; decltype(&addScalar) add = &addScalar; @@ -81,6 +83,8 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(subtract1) SIMD_OP(multiplyAdd) SIMD_OP(multiplyAdd1) + SIMD_OP(multiplyMul) + SIMD_OP(multiplyMul1) SIMD_OP(copy) SIMD_OP(cumsum) SIMD_OP(diff) @@ -118,6 +122,8 @@ void SIMDDispatch::setStatus(SIMDOps op, bool enable) SIMD_OP(subtract1) SIMD_OP(multiplyAdd) SIMD_OP(multiplyAdd1) + SIMD_OP(multiplyMul) + SIMD_OP(multiplyMul1) SIMD_OP(copy) SIMD_OP(cumsum) SIMD_OP(diff) @@ -158,6 +164,8 @@ void SIMDDispatch::resetStatus() setStatus(SIMDOps::subtract1, false); setStatus(SIMDOps::multiplyAdd, false); setStatus(SIMDOps::multiplyAdd1, false); + setStatus(SIMDOps::multiplyMul, false); + setStatus(SIMDOps::multiplyMul1, false); setStatus(SIMDOps::copy, false); setStatus(SIMDOps::cumsum, true); setStatus(SIMDOps::diff, false); @@ -243,6 +251,18 @@ void multiplyAdd1(float gain, const float* input, float* output, unsigned return simdDispatch().multiplyAdd1(gain, input, output, size); } +template <> +void multiplyMul(const float* gain, const float* input, float* output, unsigned size) noexcept +{ + return simdDispatch().multiplyMul(gain, input, output, size); +} + +template <> +void multiplyMul1(float gain, const float* input, float* output, unsigned size) noexcept +{ + return simdDispatch().multiplyMul1(gain, input, output, size); +} + template <> float linearRamp(float* output, float start, float step, unsigned size) noexcept { diff --git a/src/sfizz/SIMDHelpers.h b/src/sfizz/SIMDHelpers.h index ae34f732..08371613 100644 --- a/src/sfizz/SIMDHelpers.h +++ b/src/sfizz/SIMDHelpers.h @@ -52,6 +52,8 @@ enum class SIMDOps { subtract1, multiplyAdd, multiplyAdd1, + multiplyMul, + multiplyMul1, copy, cumsum, diff, @@ -322,6 +324,56 @@ void multiplyAdd1(T gain, absl::Span input, absl::Span output) noexc multiplyAdd1(gain, input.data(), output.data(), minSpanSize(input, output)); } +/** + * @brief Applies a gain to the input and multiply the output with it + * + * @tparam T the underlying type + * @param gain + * @param input + * @param output + * @param size + */ +template +void multiplyMul(const T* gain, const T* input, T* output, unsigned size) noexcept +{ + multiplyMulScalar(gain, input, output, size); +} + +template <> +void multiplyMul(const float* gain, const float* input, float* output, unsigned size) noexcept; + +template +void multiplyMul(absl::Span gain, absl::Span input, absl::Span output) noexcept +{ + CHECK_SPAN_SIZES(gain, input, output); + multiplyMul(gain.data(), input.data(), output.data(), minSpanSize(gain, input, output)); +} + +/** + * @brief Applies a fixed gain to the input and multiply the output with it + * + * @tparam T the underlying type + * @param gain + * @param input + * @param output + * @param size + */ +template +void multiplyMul1(T gain, const T* input, T* output, unsigned size) noexcept +{ + multiplyMul1Scalar(gain, input, output, size); +} + +template <> +void multiplyMul1(float gain, const float* input, float* output, unsigned size) noexcept; + +template +void multiplyMul1(T gain, absl::Span input, absl::Span output) noexcept +{ + CHECK_SPAN_SIZES(input, output); + multiplyMul1(gain, input.data(), output.data(), minSpanSize(input, output)); +} + /** * @brief Compute a linear ramp blockwise between 2 values * diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 643f168d..a8c1f8fe 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -355,16 +355,14 @@ float* ModMatrix::getModulation(TargetId targetId) } else { if (targetFlags & kModIsMultiplicative) { - for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= sourceDepth * sourceBuffer[i]; + multiplyMul1(sourceDepth, sourceBuffer, buffer); } else if (targetFlags & kModIsPercentMultiplicative) { - for (uint32_t i = 0; i < numFrames; ++i) - buffer[i] *= (0.01f * sourceDepth) * sourceBuffer[i]; + multiplyMul1(0.01f * sourceDepth, sourceBuffer, buffer); } else { ASSERT(targetFlags & kModIsAdditive); - sfz::multiplyAdd1(sourceDepth, sourceBuffer, buffer); + multiplyAdd1(sourceDepth, sourceBuffer, buffer); } } } diff --git a/src/sfizz/simd/HelpersSSE.cpp b/src/sfizz/simd/HelpersSSE.cpp index d17288e7..4f8daf25 100644 --- a/src/sfizz/simd/HelpersSSE.cpp +++ b/src/sfizz/simd/HelpersSSE.cpp @@ -183,6 +183,49 @@ void multiplyAdd1SSE(float gain, const float* input, float* output, unsigned siz *output++ += gain * (*input++); } +void multiplyMulSSE(const float* gain, const float* input, float* output, unsigned size) noexcept +{ + const auto sentinel = output + size; + +#if SFIZZ_HAVE_SSE2 + const auto* lastAligned = prevAligned(sentinel); + while (unaligned(input, output) && output < lastAligned) + *output++ *= (*gain++) * (*input++); + + while (output < lastAligned) { + auto mmOut = _mm_load_ps(output); + mmOut = _mm_mul_ps(_mm_mul_ps(_mm_load_ps(gain), _mm_load_ps(input)), mmOut); + _mm_store_ps(output, mmOut); + incrementAll(gain, input, output); + } +#endif + + while (output < sentinel) + *output++ *= (*gain++) * (*input++); +} + +void multiplyMul1SSE(float gain, const float* input, float* output, unsigned size) noexcept +{ + const auto sentinel = output + size; + +#if SFIZZ_HAVE_SSE2 + const auto* lastAligned = prevAligned(sentinel); + while (unaligned(input, output) && output < lastAligned) + *output++ *= gain * (*input++); + + auto mmGain = _mm_set1_ps(gain); + while (output < lastAligned) { + auto mmOut = _mm_load_ps(output); + mmOut = _mm_mul_ps(_mm_mul_ps(mmGain, _mm_load_ps(input)), mmOut); + _mm_store_ps(output, mmOut); + incrementAll(input, output); + } +#endif + + while (output < sentinel) + *output++ *= gain * (*input++); +} + float linearRampSSE(float* output, float start, float step, unsigned size) noexcept { const auto sentinel = output + size; diff --git a/src/sfizz/simd/HelpersSSE.h b/src/sfizz/simd/HelpersSSE.h index 5046d914..fded8048 100644 --- a/src/sfizz/simd/HelpersSSE.h +++ b/src/sfizz/simd/HelpersSSE.h @@ -14,6 +14,8 @@ void gain1SSE(float gain, const float* input, float* output, unsigned size) noex void divideSSE(const float* input, const float* divisor, float* output, unsigned size) noexcept; void multiplyAddSSE(const float* gain, const float* input, float* output, unsigned size) noexcept; void multiplyAdd1SSE(float gain, const float* input, float* output, unsigned size) noexcept; +void multiplyMulSSE(const float* gain, const float* input, float* output, unsigned size) noexcept; +void multiplyMul1SSE(float gain, const float* input, float* output, unsigned size) noexcept; float linearRampSSE(float* output, float start, float step, unsigned size) noexcept; float multiplicativeRampSSE(float* output, float start, float step, unsigned size) noexcept; void addSSE(const float* input, float* output, unsigned size) noexcept; diff --git a/src/sfizz/simd/HelpersScalar.h b/src/sfizz/simd/HelpersScalar.h index 871a9ff6..33c4af3f 100644 --- a/src/sfizz/simd/HelpersScalar.h +++ b/src/sfizz/simd/HelpersScalar.h @@ -67,6 +67,22 @@ inline void multiplyAdd1Scalar(T gain, const T* input, T* output, unsigned size) *output++ += gain * (*input++); } +template +inline void multiplyMulScalar(const T* gain, const T* input, T* output, unsigned size) noexcept +{ + const auto sentinel = output + size; + while (output < sentinel) + *output++ *= (*gain++) * (*input++); +} + +template +inline void multiplyMul1Scalar(T gain, const T* input, T* output, unsigned size) noexcept +{ + const auto sentinel = output + size; + while (output < sentinel) + *output++ *= gain * (*input++); +} + template T linearRampScalar(T* output, T start, T step, unsigned size) noexcept { diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index f0ef8277..27502881 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -517,6 +517,7 @@ TEST_CASE("[Helpers] MultiplyAdd (SIMD)") REQUIRE(output == expected); } + TEST_CASE("[Helpers] MultiplyAdd (SIMD vs scalar)") { std::vector gain(bigBufferSize); @@ -574,6 +575,84 @@ TEST_CASE("[Helpers] MultiplyAdd fixed gain (SIMD vs scalar)") REQUIRE(approxEqual(outputScalar, outputSIMD)); } +TEST_CASE("[Helpers] MultiplyMul (Scalar)") +{ + std::array gain { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f }; + std::array input { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + std::array output { 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }; + std::array expected { 0.0f, 0.8f, 1.8f, 2.4f, 2.0f }; + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, true); + sfz::multiplyMul(gain, input, absl::MakeSpan(output)); + REQUIRE(approxEqual(output, expected)); +} + +TEST_CASE("[Helpers] MultiplyMul (SIMD)") +{ + std::array gain { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f }; + std::array input { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + std::array output { 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }; + std::array expected { 0.0f, 0.8f, 1.8f, 2.4f, 2.0f }; + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, true); + sfz::multiplyMul(gain, input, absl::MakeSpan(output)); + REQUIRE(approxEqual(output, expected)); +} + +TEST_CASE("[Helpers] MultiplyMul (SIMD vs Scalar)") +{ + std::vector gain(bigBufferSize); + std::vector input(bigBufferSize); + std::vector outputScalar(bigBufferSize); + std::vector outputSIMD(bigBufferSize); + absl::c_iota(gain, 0.0f); + absl::c_iota(input, 0.0f); + absl::c_iota(outputScalar, 0.0f); + absl::c_iota(outputSIMD, 0.0f); + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, false); + sfz::multiplyMul(gain, input, absl::MakeSpan(outputScalar)); + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul, true); + sfz::multiplyMul(gain, input, absl::MakeSpan(outputSIMD)); + REQUIRE(approxEqual(outputScalar, outputSIMD)); +} + +TEST_CASE("[Helpers] MultiplyMul fixed gain (Scalar)") +{ + float gain = 0.3f; + std::array input { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + std::array output { 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }; + std::array expected { 1.5f, 2.4f, 2.7f, 2.4f, 1.5f }; + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, false); + sfz::multiplyMul1(gain, input, absl::MakeSpan(output)); + REQUIRE(output == expected); +} + +TEST_CASE("[Helpers] MultiplyMul fixed gain (SIMD)") +{ + float gain = 0.3f; + std::array input { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + std::array output { 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }; + std::array expected { 1.5f, 2.4f, 2.7f, 2.4f, 1.5f }; + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, true); + sfz::multiplyMul1(gain, input, absl::MakeSpan(output)); + REQUIRE(output == expected); +} + +TEST_CASE("[Helpers] MultiplyMul fixed gain (SIMD vs scalar)") +{ + float gain = 0.3f; + std::vector input(bigBufferSize); + std::vector outputScalar(bigBufferSize); + std::vector outputSIMD(bigBufferSize); + absl::c_iota(input, 0.0f); + absl::c_iota(outputScalar, 0.0f); + absl::c_iota(outputSIMD, 0.0f); + + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, false); + sfz::multiplyMul1(gain, input, absl::MakeSpan(outputScalar)); + sfz::setSIMDOpStatus(sfz::SIMDOps::multiplyMul1, true); + sfz::multiplyMul1(gain, input, absl::MakeSpan(outputSIMD)); + REQUIRE(approxEqual(outputScalar, outputSIMD)); +} + TEST_CASE("[Helpers] Subtract") { std::array input { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; From d2e8c0548fb844bcc54665ce3b7cd82f3d320f8e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 9 Sep 2020 13:06:24 +0200 Subject: [PATCH 216/445] Regions on the same note but with different groups do get muted This happens e.g. with keyswitches. This behavior is slightly different from Cakewalk's apparently. --- src/sfizz/Voice.cpp | 4 ++-- tests/SynthT.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4f120348..8647de86 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -713,9 +713,9 @@ bool sfz::Voice::checkOffGroup(const Region* other, int delay, int noteNumber) n if (region == nullptr || other == nullptr) return false; - if (triggerEvent.type == TriggerEventType::NoteOn + if (triggerEvent.type == TriggerEventType::NoteOn && region->offBy == other->group - && noteNumber != triggerEvent.number) { + && (region->group != other->group || noteNumber != triggerEvent.number)) { off(delay); return true; } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 69f722ae..65f3adb9 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1286,3 +1286,27 @@ TEST_CASE("[Synth] Off by same note and group") REQUIRE( numPlayingVoices(synth) == 2 ); synth.noteOn(0, 60, 85); } + + +TEST_CASE("[Synth] Off by with CC switches") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + synth.loadSfzString(fs::current_path(), R"( + ampeg_decay=5 ampeg_sustain=0 ampeg_release=5 key=60 + sample=*saw transpose=12 group=1 off_by=2 hicc4=63 + sample=*triangle group=2 off_by=1 locc4=64 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "*saw" ); + synth.cc(0, 4, 127); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "*triangle" ); + synth.cc(0, 4, 0); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "*saw" ); +} From 1fcd3077aef5fc7f4586af7ec9ad654781e8e489 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 9 Sep 2020 15:23:15 +0200 Subject: [PATCH 217/445] Correct the condition for the envelope to free-run --- src/sfizz/ADSREnvelope.cpp | 7 +++--- tests/SynthT.cpp | 44 +++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index 8d92ea10..419a8f55 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -54,10 +54,9 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, sustainThreshold = this->sustain + config::virtuallyZero; shouldRelease = false; freeRunning = ( - (region.trigger == SfzTrigger::release) - || (this->sustain == 0.0f) - || (region.trigger == SfzTrigger::release_key) - || (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator))); + (this->sustain == 0.0f) + || (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator)) + ); currentValue = this->start; currentState = State::Delay; } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 69f722ae..7db5fbd8 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -687,16 +687,54 @@ TEST_CASE("[Synth] Sisters and off-by") REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 ); } -TEST_CASE("[Synth] Release key") +TEST_CASE("[Synth] Release (basic behavior with sample)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + synth.setSamplesPerBlock(4096); + sfz::AudioBuffer buffer { 2, 4096 }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release_key_sample.sfz", R"( + key=62 sample=*sine + key=62 sample=closedhat.wav trigger=release_key + )"); + synth.noteOn(0, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "*sine" ); + synth.noteOff(0, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "closedhat.wav" ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "closedhat.wav" ); +} + +TEST_CASE("[Synth] Release key (basic behavior with sample)") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(4096); + sfz::AudioBuffer buffer { 2, 4096 }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release_key_sample.sfz", R"( + key=62 sample=closedhat.wav trigger=release_key + )"); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "closedhat.wav" ); +} + +TEST_CASE("[Synth] Release key (pedal)") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(4096); + sfz::AudioBuffer buffer { 2, 4096 }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release_key_pedal.sfz", R"( key=62 sample=*sine trigger=release_key )"); synth.noteOn(0, 62, 85); synth.cc(0, 64, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 1 ); + REQUIRE( numPlayingVoices(synth) == 1 ); } TEST_CASE("[Synth] Release") From 935b07603e7baf2ee8fc44c4c39bcd9638df5a24 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 10 Sep 2020 13:37:36 +0200 Subject: [PATCH 218/445] Adjust the preload value to account for 4-byte floats --- editor/src/editor/Editor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 794eb331..35772396 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -565,14 +565,14 @@ void Editor::Impl::createFrameContents() for (int log2value = 10; log2value <= 16; ++log2value) { int value = 1 << log2value; char text[256]; - sprintf(text, "%d kB", value / 1024); + sprintf(text, "%d kB", value / 1024 * 4); text[sizeof(text) - 1] = '\0'; preloadSizeSlider_->addEntry(text, value); } preloadSizeSlider_->setValueToStringFunction2( [](float value, std::string& result, CParamDisplay*) -> bool { - result = std::to_string(static_cast(std::round(value * (1.0 / 1024)))) + " kB"; + result = std::to_string(static_cast(std::round(value * (1.0 / 1024 * 4)))) + " kB"; return true; }); From d31ec18bf6435eeedd9dbe9966b919692d0ed3ef Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 10 Sep 2020 15:31:09 +0200 Subject: [PATCH 219/445] Remove the no-omit-frame-pointer option It's inconsequent in x86 more or less, but ARM has less registers to spare. --- cmake/SfizzConfig.cmake | 1 - 1 file changed, 1 deletion(-) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 02c38fc3..0e70d9e8 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -57,7 +57,6 @@ endif() if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Wall) add_compile_options(-Wextra) - add_compile_options(-fno-omit-frame-pointer) # For debugging purposes add_compile_options(-Werror=return-type) if (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(i.86|x86_64)$") add_compile_options(-msse2) From 279382a7b218b9d1b310613088b3fe1b5ad971d8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 10 Sep 2020 15:45:26 +0200 Subject: [PATCH 220/445] Clear adjustment for the preload value --- editor/src/editor/Editor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 35772396..1d7bc7b4 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -565,14 +565,14 @@ void Editor::Impl::createFrameContents() for (int log2value = 10; log2value <= 16; ++log2value) { int value = 1 << log2value; char text[256]; - sprintf(text, "%d kB", value / 1024 * 4); + sprintf(text, "%lu kB", value / 1024 * sizeof(float)); text[sizeof(text) - 1] = '\0'; preloadSizeSlider_->addEntry(text, value); } preloadSizeSlider_->setValueToStringFunction2( [](float value, std::string& result, CParamDisplay*) -> bool { - result = std::to_string(static_cast(std::round(value * (1.0 / 1024 * 4)))) + " kB"; + result = std::to_string(static_cast(std::round(value * (1.0 / 1024 * sizeof(float))))) + " kB"; return true; }); From 59c64bcb91017fd9965ddf4865f4630b9b18c7e2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 03:23:39 +0200 Subject: [PATCH 221/445] Tweaking the GUI The instrument information can be on the main tab Put the button behind the logo Embed the ABeeZee font and use it everywhere Add hover colors to glyph buttons Replace the "file open" glyph by a folder --- editor/CMakeLists.txt | 3 + editor/layout/main.fl | 174 +++++-------- editor/resources/Fonts/ABeeZee-Italic.ttf | Bin 0 -> 45780 bytes editor/resources/Fonts/ABeeZee-Regular.ttf | Bin 0 -> 44184 bytes editor/resources/logo_full_white.png | Bin 0 -> 4516 bytes editor/src/editor/Editor.cpp | 80 ++++-- editor/src/editor/GUIComponents.cpp | 3 + editor/src/editor/layout/main.hpp | 288 ++++++++++----------- 8 files changed, 269 insertions(+), 279 deletions(-) create mode 100644 editor/resources/Fonts/ABeeZee-Italic.ttf create mode 100644 editor/resources/Fonts/ABeeZee-Regular.ttf create mode 100644 editor/resources/logo_full_white.png diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index cce49479..935f5c79 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -5,11 +5,14 @@ set(EDITOR_RESOURCES logo.png logo_text.png logo_text@2x.png + logo_full_white.png icon_white.png icon_white@2x.png knob48.png knob48@2x.png Fonts/fluentui-system-regular-20.ttf + Fonts/ABeeZee-Regular.ttf + Fonts/ABeeZee-Regular.ttf PARENT_SCOPE) function(copy_editor_resources SOURCE_DIR DESTINATION_DIR) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index 1d27e987..477d2d40 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -1,9 +1,9 @@ # data file for the Fltk User Interface Designer (fluid) -version 1.0305 +version 1.0304 header_name {.h} code_name {.cxx} widget_class mainView {open - xywh {410 523 800 475} type Double + xywh {572 266 800 475} type Double class LogicalGroup visible } { Fl_Group {} { @@ -12,112 +12,104 @@ widget_class mainView {open class LogicalGroup } { Fl_Group {} {open - xywh {5 4 100 101} box ROUNDED_BOX align 0 + xywh {5 4 175 101} box ROUNDED_BOX align 0 class RoundedGroup } { Fl_Box {} { comment {tag=kTagFirstChangePanel+kPanelGeneral} - image {../resources/icon_white.png} xywh {7 6 96 96} + image {../resources/logo_full_white.png} xywh {10 11 165 63} class SfizzMainButton } + Fl_Button {} { + comment {tag=kTagFirstChangePanel+kPanelGeneral} + xywh {55 75 25 25} labelsize 24 + class HomeButton + } + Fl_Button {} { + comment {tag=kTagFirstChangePanel+kPanelControls} + xywh {80 75 25 25} labelsize 24 + class CCButton + } + Fl_Button {} { + comment {tag=kTagFirstChangePanel+kPanelSettings} + xywh {105 75 25 25} labelsize 24 + class SettingsButton + } } Fl_Group {} {open - xywh {110 5 380 100} box ROUNDED_BOX + xywh {185 5 380 100} box ROUNDED_BOX class RoundedGroup } { Fl_Box {} { label {File:} - xywh {125 15 40 25} labelsize 16 + xywh {200 13 40 30} labelsize 16 class Label } Fl_Box {} { label {KS:} - xywh {125 45 40 25} labelsize 16 + xywh {200 45 40 30} labelsize 16 class Label } Fl_Box {} { label {Separator 1} - xywh {120 40 355 5} box BORDER_BOX labeltype NO_LABEL + xywh {195 41 360 5} box BORDER_BOX labeltype NO_LABEL class HLine } Fl_Box {} { label {Separator 2} - xywh {120 70 355 5} box BORDER_BOX labeltype NO_LABEL + xywh {195 73 360 5} box BORDER_BOX labeltype NO_LABEL class HLine } Fl_Box sfzFileLabel_ { label {DefaultInstrument.sfz} - xywh {190 15 230 25} labelsize 20 + xywh {265 12 230 30} labelsize 20 class Label } Fl_Box {} { label {Key switch} - xywh {190 45 230 25} labelsize 20 + xywh {265 44 230 30} labelsize 20 class Label } Fl_Box {} { label {Voices:} - xywh {120 75 60 25} labelsize 12 align 24 + xywh {195 76 60 25} labelsize 12 align 24 class Label } Fl_Button {} { comment {tag=kTagLoadSfzFile} - xywh {425 15 25 25} labelsize 24 + xywh {500 14 25 25} labelsize 24 class LoadFileButton } Fl_Button {} { comment {tag=kTagEditSfzFile} - xywh {450 15 25 25} labelsize 24 + xywh {525 14 25 25} labelsize 24 class EditFileButton } Fl_Box infoVoicesLabel_ { - xywh {185 75 50 25} labelsize 12 align 16 + xywh {260 76 50 25} labelsize 12 align 16 class Label } Fl_Box {} { label {Max:} - xywh {240 75 60 25} labelsize 12 align 24 + xywh {315 76 60 25} labelsize 12 align 24 class Label } Fl_Box numVoicesLabel_ { - xywh {305 75 50 25} labelsize 12 align 16 + xywh {380 76 50 25} labelsize 12 align 16 class Label } Fl_Box {} { label {Memory:} - xywh {360 75 60 25} labelsize 12 align 24 + xywh {435 76 60 25} labelsize 12 align 24 class Label } Fl_Box memoryLabel_ { - xywh {425 75 50 25} labelsize 12 align 16 + xywh {500 76 50 25} labelsize 12 align 16 class Label } } Fl_Group {} {open - xywh {495 5 100 100} box ROUNDED_BOX - class RoundedGroup - } { - Fl_Light_Button {} { - label SETUP - comment {tag=kTagFirstChangePanel+kPanelSettings} - xywh {510 42 70 25} - class LightButton - } - Fl_Light_Button {} { - label CC - comment {tag=kTagFirstChangePanel+kPanelControls} - xywh {510 15 70 25} - class LightButton - } - Fl_Light_Button {} { - label INFO - comment {tag=kTagFirstChangePanel+kPanelInfo} - xywh {510 69 70 25} - class LightButton - } - } - Fl_Group {} {open - xywh {600 5 195 100} box ROUNDED_BOX + xywh {570 5 225 100} box ROUNDED_BOX class RoundedGroup } { Fl_Dial {} { @@ -126,7 +118,7 @@ widget_class mainView {open } Fl_Box {} { label Center - xywh {610 70 60 25} labelsize 12 hide + xywh {610 70 60 5} labelsize 12 hide class ValueLabel } Fl_Dial volumeSlider_ { @@ -136,83 +128,74 @@ widget_class mainView {open } Fl_Box volumeLabel_ { label {0.0 dB} - xywh {675 70 60 25} labelsize 12 + xywh {675 70 60 22} labelsize 12 class ValueLabel } Fl_Box {} { - xywh {745 20 35 70} box BORDER_BOX + xywh {745 20 35 55} box BORDER_BOX class VMeter } } } - Fl_Group {subPanels_[kPanelGeneral]} { - xywh {5 110 790 285} hide + Fl_Group {subPanels_[kPanelGeneral]} {open selected + xywh {5 110 791 285} class LogicalGroup } { Fl_Group {} {open - xywh {5 110 120 285} box ROUNDED_BOX + xywh {5 110 175 280} box ROUNDED_BOX class RoundedGroup } { Fl_Box {} { label {Curves:} - xywh {15 120 60 25} labelsize 12 align 20 + xywh {20 120 60 25} align 20 class Label } Fl_Box {} { label {Masters:} - xywh {15 145 60 25} labelsize 12 align 20 + xywh {20 145 60 25} align 20 class Label } Fl_Box {} { label {Groups:} - xywh {15 170 60 25} labelsize 12 align 20 + xywh {20 170 60 25} align 20 class Label } Fl_Box {} { label {Regions:} - xywh {15 195 60 25} labelsize 12 align 20 + xywh {20 195 60 25} align 20 class Label } Fl_Box {} { label {Samples:} - xywh {15 220 60 25} labelsize 12 align 20 + xywh {20 220 60 25} align 20 class Label } Fl_Box infoCurvesLabel_ { label 0 - xywh {75 120 40 25} labelsize 12 align 16 + xywh {120 120 40 25} align 16 class Label } Fl_Box infoMastersLabel_ { label 0 - xywh {75 145 40 25} labelsize 12 align 16 + xywh {120 145 40 25} align 16 class Label } Fl_Box infoGroupsLabel_ { label 0 - xywh {75 170 40 25} labelsize 12 align 16 + xywh {120 170 40 25} align 16 class Label } Fl_Box infoRegionsLabel_ { label 0 - xywh {75 195 40 25} labelsize 12 align 16 + xywh {120 195 40 25} align 16 class Label } Fl_Box infoSamplesLabel_ { label 0 - xywh {75 220 40 25} labelsize 12 align 16 + xywh {120 220 40 25} align 16 class Label } } - Fl_Group {} {open - xywh {130 110 665 280} - class LogicalGroup - } { - Fl_Box {} { - image {../resources/logo_text.png} xywh {260 125 400 250} - class SfizzLargePicture - } - } } Fl_Group {subPanels_[kPanelControls]} { xywh {5 110 790 285} hide @@ -229,114 +212,99 @@ widget_class mainView {open } } } - Fl_Group {subPanels_[kPanelSettings]} {open - xywh {5 110 790 285} + Fl_Group {subPanels_[kPanelSettings]} { + xywh {5 109 790 286} hide class LogicalGroup } { Fl_Group {} { - label Engine open selected - xywh {260 125 280 110} box ROUNDED_BOX labelsize 12 align 17 + label Engine open + xywh {260 110 280 110} box ROUNDED_BOX labelsize 12 align 17 class TitleGroup } { Fl_Spinner numVoicesSlider_ { comment {tag=kTagSetNumVoices} - xywh {285 185 60 25} labelsize 12 textsize 12 + xywh {285 170 60 25} labelsize 12 textsize 12 class ValueMenu } Fl_Box {} { label Polyphony - xywh {275 145 80 25} labelsize 12 + xywh {275 130 80 25} labelsize 12 class ValueLabel } Fl_Spinner oversamplingSlider_ { comment {tag=kTagSetOversampling} - xywh {370 185 60 25} labelsize 12 textsize 12 + xywh {370 170 60 25} labelsize 12 textsize 12 class ValueMenu } Fl_Box {} { label Oversampling - xywh {360 145 80 25} labelsize 12 + xywh {360 130 80 25} labelsize 12 class ValueLabel } Fl_Box {} { label {Preload size} - xywh {445 145 80 25} labelsize 12 + xywh {445 130 80 25} labelsize 12 class ValueLabel } Fl_Spinner preloadSizeSlider_ { comment {tag=kTagSetPreloadSize} - xywh {455 185 60 25} labelsize 12 textsize 12 + xywh {455 170 60 25} labelsize 12 textsize 12 class ValueMenu } } Fl_Group {} { label Tuning open - xywh {205 260 390 120} box ROUNDED_BOX labelsize 12 align 17 + xywh {205 268 390 120} box ROUNDED_BOX labelsize 12 align 17 class TitleGroup } { Fl_Box {} { label {Root key} - xywh {330 280 80 25} labelsize 12 + xywh {330 288 80 25} labelsize 12 class ValueLabel } Fl_Spinner tuningFrequencySlider_ { comment {tag=kTagSetTuningFrequency} - xywh {425 320 60 25} labelsize 12 textsize 12 + xywh {425 328 60 25} labelsize 12 textsize 12 class ValueMenu } Fl_Box {} { label Frequency - xywh {415 280 80 25} labelsize 12 + xywh {415 288 80 25} labelsize 12 class ValueLabel } Fl_Dial stretchedTuningSlider_ { comment {tag=kTagSetStretchedTuning} - xywh {515 305 48 48} value 0.5 + xywh {515 313 48 48} value 0.5 class Knob48 } Fl_Box {} { label Stretch - xywh {500 280 80 25} labelsize 12 + xywh {500 288 80 25} labelsize 12 class ValueLabel } Fl_Box {} { label {Scala file} - xywh {225 280 100 25} labelsize 12 + xywh {225 288 100 25} labelsize 12 class ValueLabel } Fl_Button scalaFileButton_ { label DefaultScale comment {tag=kTagLoadScalaFile} - xywh {225 320 100 25} labelsize 12 + xywh {225 328 100 25} labelsize 12 class ValueButton } Fl_Spinner scalaRootKeySlider_ { comment {tag=kTagSetScalaRootKey} - xywh {340 320 35 25} labelsize 12 textsize 12 + xywh {340 328 35 25} labelsize 12 textsize 12 class ValueMenu } Fl_Spinner scalaRootOctaveSlider_ { comment {tag=kTagSetScalaRootKey} - xywh {375 320 30 25} labelsize 12 textsize 12 + xywh {375 328 30 25} labelsize 12 textsize 12 class ValueMenu } } } - Fl_Group {subPanels_[kPanelInfo]} { - xywh {5 110 790 285} hide - class LogicalGroup - } { - Fl_Group {} {open - xywh {5 110 790 285} box ROUNDED_BOX - class RoundedGroup - } { - Fl_Box {} { - label {Informative text goes here} - xywh {5 110 790 285} labelsize 40 - class Label - } - } - } Fl_Box {} { xywh {5 400 790 70} class Piano diff --git a/editor/resources/Fonts/ABeeZee-Italic.ttf b/editor/resources/Fonts/ABeeZee-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d98743aff8bc049de88675e3c7efbfd89264d8f5 GIT binary patch literal 45780 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 literal 0 HcmV?d00001 diff --git a/editor/resources/Fonts/ABeeZee-Regular.ttf b/editor/resources/Fonts/ABeeZee-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..71e6d16245f37eb1e7aecd847f4c4ae54c9e836d GIT binary patch literal 44184 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 @+H{4m&%{0J+I_`J9Vk_k+ss_AGuWe z=vwLR$Ja{5_O&nmj=LLQ7AYD^JOn$i6=S_|;D%vG_D8Xcy6H3@G_sF^qez%UaKYAq z8>PvaGdZyblNL%Ji{$dLKs5;IzvVJq39^S*8hdgiCE)jJCWX*^zL{Cv&F-it0LwxCLwR1sh_6sh+Vu{tHr+ zG5<%SrDIdMTZ%5Og&g;Hwhei5UD0%Z!L3z~Z8ICH?{%bm(pq(ECe_5>DaRle;31%> z_9^EPh+3?YRYds$e-?;W zAnh2hfN~{XsViUN?paq(yh6`|S2`{|O~q+ z*hZF)Jm*P*TlS=y zrvqYJVUv1bN)wpK`&yDNN4!ahYeMONj4o}kp2B3f~*vKNw_?UQj&XbXE@tfzQo_ z&jXhOmujK9rPAN;x>QP-kVW?N<4cxp@$jv zBw0`}iq1YzIFGfX;3F@%3+X~fVXQD)SSXw3$4`h23xo}_WLg;yU0K8Q9Soxz%a^!Q>&gk!^gJ*< zc&Qe`sZ@IVekzsXlZcA0VRbJ7%fxllurd;`hA0;FZIULd8{^i+Y%x1SHagbiD-!o! zIv)k$pe(}*;d%(3c&Q94hgHgpK%|aiLwxLuXIRxDWBE{hSnN;4<^NirU`27U{Qs^C zid};bL5<*}6HxNKmXv>CCOS z)@Mp=+cqb>Vep;L%a#})+zZUF{u~`ivr(2bA{AH9kUhQn7|O#;+DKCP_n)91Lk4jl z$K1x+`WVblD)XXjXiA9{NTv|ffE}&yN}hCiY}?1%7=8A@U-lK`R3y0rt0cJ&(mXAm zr`0qbXJh0(d`$&Qew>Ywk%Pf+0<@FdZ$Cmfz?#x{dU`cXc@MzMA$pp-$lZk}C?Dc4 zSd+^H9My7XS>xoE)|q2<@$<|!gE@vY36>?GW(OMkIxsA9sjvp(^xP92-~LSxdM2p< z^H&|;_({*jXQ-b55LUxLO9og=p5%T}eHi87PyQ9G-G^CuoO?T)!BBvDRDg>C@{JKl zU0SgW%N2oRYh0+=f%0eAS{fLYVS)uMLVl>KdT7Ld`N2p0zvBmZoqs6BUtazC?x90J zA&e0;$;a)X(F}(DWM#5|C+Z_-bd$_{86k#t_9PqDs%m~nc^RxW)|Ruc3f)(i(y=Lo zB%RN8Z65ply3~$$`!;9!J%l$v3!{7THATbtWq$CpP� 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 iconWhite = owned(new CBitmap("icon_white.png")); + SharedPointer iconWhite = owned(new CBitmap("logo_full_white.png")); SharedPointer knob48 = owned(new CBitmap("knob48.png")); SharedPointer logoText = owned(new CBitmap("logo_text.png")); @@ -332,6 +357,7 @@ void Editor::Impl::createFrameContents() CColor titleBoxText; CColor titleBoxBackground; CColor icon; + CColor iconHighlight; CColor valueText; CColor valueBackground; }; @@ -342,6 +368,7 @@ void Editor::Impl::createFrameContents() lightTheme.titleBoxText = { 0xff, 0xff, 0xff }; lightTheme.titleBoxBackground = { 0x2e, 0x34, 0x36 }; lightTheme.icon = lightTheme.text; + lightTheme.iconHighlight = { 0xa8, 0x62, 0x34 }; lightTheme.valueText = { 0xff, 0xff, 0xff }; lightTheme.valueBackground = { 0x2e, 0x34, 0x36 }; Theme darkTheme; @@ -350,6 +377,7 @@ void Editor::Impl::createFrameContents() darkTheme.titleBoxText = { 0x00, 0x00, 0x00 }; darkTheme.titleBoxBackground = { 0xba, 0xbd, 0xb6 }; darkTheme.icon = darkTheme.text; + darkTheme.iconHighlight = { 0xa8, 0x62, 0x34 }; darkTheme.valueText = { 0x2e, 0x34, 0x36 }; darkTheme.valueBackground = { 0xff, 0xff, 0xff }; Theme& defaultTheme = lightTheme; @@ -363,18 +391,19 @@ void Editor::Impl::createFrameContents() typedef CKickButton SfizzMainButton; typedef CTextLabel Label; typedef CViewContainer HLine; - typedef CTextButton LightButton; typedef CAnimKnob Knob48; typedef CTextLabel ValueLabel; typedef CViewContainer VMeter; - typedef CView SfizzLargePicture; typedef SValueMenu ValueMenu; #if 0 typedef CTextButton Button; #endif typedef CTextButton ValueButton; - typedef CTextButton LoadFileButton; - typedef CTextButton EditFileButton; + typedef CHoverButton LoadFileButton; + typedef CHoverButton CCButton; + typedef CHoverButton HomeButton; + typedef CHoverButton SettingsButton; + typedef CHoverButton EditFileButton; typedef SPiano Piano; auto createLogicalGroup = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { @@ -394,7 +423,7 @@ void Editor::Impl::createFrameContents() box->setBackgroundColor(theme->boxBackground); box->setTitleFontColor(theme->titleBoxText); box->setTitleBackgroundColor(theme->titleBoxBackground); - auto font = owned(new CFontDesc(*box->getTitleFont())); + auto font = owned(new CFontDesc("ABeeZee", fontsize)); font->setSize(fontsize); box->setTitleFont(font); return box; @@ -408,7 +437,7 @@ void Editor::Impl::createFrameContents() lbl->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); lbl->setFontColor(theme->text); lbl->setHoriAlign(align); - auto font = owned(new CFontDesc(*lbl->getFont())); + auto font = owned(new CFontDesc("ABeeZee", fontsize)); font->setSize(fontsize); lbl->setFont(font); return lbl; @@ -420,11 +449,6 @@ void Editor::Impl::createFrameContents() hline->setBackgroundColor(CColor(0xff, 0xff, 0xff, 0xff)); return hline; }; - auto createLightButton = [this](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int) { - CTextButton* button = new CTextButton(bounds, this, tag, label); - button->setTextAlignment(align); - return button; - }; auto createKnob48 = [this, &knob48](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int) { return new CAnimKnob(bounds, this, tag, 31, 48, knob48); }; @@ -434,7 +458,7 @@ void Editor::Impl::createFrameContents() lbl->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); lbl->setFontColor(theme->text); lbl->setHoriAlign(align); - auto font = owned(new CFontDesc(*lbl->getFont())); + auto font = owned(new CFontDesc("ABeeZee", fontsize)); font->setSize(fontsize); lbl->setFont(font); return lbl; @@ -445,11 +469,6 @@ void Editor::Impl::createFrameContents() container->setBackgroundColor(CColor(0x00, 0x00, 0x00, 0x00)); return container; }; - auto createSfizzLargePicture = [&logoText](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { - CView* picture = new CView(bounds); - picture->setBackground(logoText); - return picture; - }; #if 0 auto createButton = [this](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { CTextButton* button = new CTextButton(bounds, this, tag, label); @@ -462,7 +481,7 @@ void Editor::Impl::createFrameContents() #endif auto createValueButton = [this, &theme](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { CTextButton* button = new CTextButton(bounds, this, tag, label); - auto font = owned(new CFontDesc(*button->getFont())); + auto font = owned(new CFontDesc("ABeeZee", fontsize)); font->setSize(fontsize); button->setFont(font); button->setTextAlignment(align); @@ -476,7 +495,7 @@ void Editor::Impl::createFrameContents() auto createValueMenu = [this, &theme](const CRect& bounds, int tag, const char*, CHoriTxtAlign align, int fontsize) { SValueMenu* vm = new SValueMenu(bounds, this, tag); vm->setHoriAlign(align); - auto font = owned(new CFontDesc(*vm->getFont())); + auto font = owned(new CFontDesc("ABeeZee", fontsize)); font->setSize(fontsize); vm->setFont(font); vm->setFontColor(theme->valueText); @@ -487,19 +506,30 @@ void Editor::Impl::createFrameContents() return vm; }; auto createGlyphButton = [this, &theme](UTF8StringPtr glyph, const CRect& bounds, int tag, int fontsize) { - CTextButton* btn = new CTextButton(bounds, this, tag, glyph); + CHoverButton* btn = new CHoverButton(bounds, this, tag, glyph); btn->setFont(new CFontDesc("Fluent System Regular W20", fontsize)); btn->setTextColor(theme->icon); + btn->setHoverColor(theme->iconHighlight); btn->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); btn->setGradient(nullptr); btn->setGradientHighlighted(nullptr); return btn; }; - auto createLoadFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { - return createGlyphButton(u8"\ue142", bounds, tag, fontsize); + auto createHomeButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + return createGlyphButton(u8"\ue1d6", bounds, tag, fontsize); + }; + auto createCCButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + // return createGlyphButton(u8"\ue240", bounds, tag, fontsize); + return createGlyphButton(u8"\ue140", bounds, tag, fontsize); + }; + auto createSettingsButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + return createGlyphButton(u8"\ue2e4", bounds, tag, fontsize); }; auto createEditFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { - return createGlyphButton(u8"\ue148", bounds, tag, fontsize); + return createGlyphButton(u8"\ue142", bounds, tag, fontsize); + }; + auto createLoadFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + return createGlyphButton(u8"\ue1a3", bounds, tag, fontsize); }; auto createPiano = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { SPiano* piano = new SPiano(bounds); diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index c4e6f958..d604d76e 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -408,6 +408,9 @@ CMouseEventResult SValueMenu::onMouseDown(CPoint& where, const CButtonState& but menu->addEntry(item); item->remember(); // above call does not increment refcount } + menu->setFont(self->getFont()); + menu->setFontColor(self->getFontColor()); + menu->setBackColor(self->getBackColor()); menu->popup(frame, frameWhere + CPoint(0.0, 1.0)); } }); diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index 808e5a10..c8e93a39 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -4,164 +4,150 @@ mainView = view__0; enterTheme(darkTheme); LogicalGroup* const view__1 = createLogicalGroup(CRect(0, 0, 800, 110), -1, "", kCenterText, 14); view__0->addView(view__1); -RoundedGroup* const view__2 = createRoundedGroup(CRect(5, 4, 105, 105), -1, "", kCenterText, 14); +RoundedGroup* const view__2 = createRoundedGroup(CRect(5, 4, 180, 105), -1, "", kCenterText, 14); view__1->addView(view__2); -SfizzMainButton* const view__3 = createSfizzMainButton(CRect(2, 2, 98, 98), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 14); +SfizzMainButton* const view__3 = createSfizzMainButton(CRect(5, 7, 170, 70), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 14); view__2->addView(view__3); -RoundedGroup* const view__4 = createRoundedGroup(CRect(110, 5, 490, 105), -1, "", kCenterText, 14); -view__1->addView(view__4); -Label* const view__5 = createLabel(CRect(15, 10, 55, 35), -1, "File:", kCenterText, 16); -view__4->addView(view__5); -Label* const view__6 = createLabel(CRect(15, 40, 55, 65), -1, "KS:", kCenterText, 16); -view__4->addView(view__6); -HLine* const view__7 = createHLine(CRect(10, 35, 365, 40), -1, "", kCenterText, 14); -view__4->addView(view__7); -HLine* const view__8 = createHLine(CRect(10, 65, 365, 70), -1, "", kCenterText, 14); -view__4->addView(view__8); -Label* const view__9 = createLabel(CRect(80, 10, 310, 35), -1, "DefaultInstrument.sfz", kCenterText, 20); -sfzFileLabel_ = view__9; -view__4->addView(view__9); -Label* const view__10 = createLabel(CRect(80, 40, 310, 65), -1, "Key switch", kCenterText, 20); -view__4->addView(view__10); -Label* const view__11 = createLabel(CRect(10, 70, 70, 95), -1, "Voices:", kRightText, 12); -view__4->addView(view__11); -LoadFileButton* const view__12 = createLoadFileButton(CRect(315, 10, 340, 35), kTagLoadSfzFile, "", kCenterText, 24); -view__4->addView(view__12); -EditFileButton* const view__13 = createEditFileButton(CRect(340, 10, 365, 35), kTagEditSfzFile, "", kCenterText, 24); -view__4->addView(view__13); -Label* const view__14 = createLabel(CRect(75, 70, 125, 95), -1, "", kCenterText, 12); -infoVoicesLabel_ = view__14; -view__4->addView(view__14); -Label* const view__15 = createLabel(CRect(130, 70, 190, 95), -1, "Max:", kRightText, 12); -view__4->addView(view__15); -Label* const view__16 = createLabel(CRect(195, 70, 245, 95), -1, "", kCenterText, 12); -numVoicesLabel_ = view__16; -view__4->addView(view__16); -Label* const view__17 = createLabel(CRect(250, 70, 310, 95), -1, "Memory:", kRightText, 12); -view__4->addView(view__17); -Label* const view__18 = createLabel(CRect(315, 70, 365, 95), -1, "", kCenterText, 12); -memoryLabel_ = view__18; -view__4->addView(view__18); -RoundedGroup* const view__19 = createRoundedGroup(CRect(495, 5, 595, 105), -1, "", kCenterText, 14); -view__1->addView(view__19); -LightButton* const view__20 = createLightButton(CRect(15, 37, 85, 62), kTagFirstChangePanel+kPanelSettings, "SETUP", kCenterText, 14); -view__19->addView(view__20); -LightButton* const view__21 = createLightButton(CRect(15, 10, 85, 35), kTagFirstChangePanel+kPanelControls, "CC", kCenterText, 14); -view__19->addView(view__21); -LightButton* const view__22 = createLightButton(CRect(15, 64, 85, 89), kTagFirstChangePanel+kPanelInfo, "INFO", kCenterText, 14); -view__19->addView(view__22); -RoundedGroup* const view__23 = createRoundedGroup(CRect(600, 5, 795, 105), -1, "", kCenterText, 14); -view__1->addView(view__23); -Knob48* const view__24 = createKnob48(CRect(15, 15, 63, 63), -1, "", kCenterText, 14); -view__23->addView(view__24); +HomeButton* const view__4 = createHomeButton(CRect(50, 71, 75, 96), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 24); +view__2->addView(view__4); +CCButton* const view__5 = createCCButton(CRect(75, 71, 100, 96), kTagFirstChangePanel+kPanelControls, "", kCenterText, 24); +view__2->addView(view__5); +SettingsButton* const view__6 = createSettingsButton(CRect(100, 71, 125, 96), kTagFirstChangePanel+kPanelSettings, "", kCenterText, 24); +view__2->addView(view__6); +RoundedGroup* const view__7 = createRoundedGroup(CRect(185, 5, 565, 105), -1, "", kCenterText, 14); +view__1->addView(view__7); +Label* const view__8 = createLabel(CRect(15, 8, 55, 38), -1, "File:", kCenterText, 16); +view__7->addView(view__8); +Label* const view__9 = createLabel(CRect(15, 40, 55, 70), -1, "KS:", kCenterText, 16); +view__7->addView(view__9); +HLine* const view__10 = createHLine(CRect(10, 36, 370, 41), -1, "", kCenterText, 14); +view__7->addView(view__10); +HLine* const view__11 = createHLine(CRect(10, 68, 370, 73), -1, "", kCenterText, 14); +view__7->addView(view__11); +Label* const view__12 = createLabel(CRect(80, 7, 310, 37), -1, "DefaultInstrument.sfz", kCenterText, 20); +sfzFileLabel_ = view__12; +view__7->addView(view__12); +Label* const view__13 = createLabel(CRect(80, 39, 310, 69), -1, "Key switch", kCenterText, 20); +view__7->addView(view__13); +Label* const view__14 = createLabel(CRect(10, 71, 70, 96), -1, "Voices:", kRightText, 12); +view__7->addView(view__14); +LoadFileButton* const view__15 = createLoadFileButton(CRect(315, 9, 340, 34), kTagLoadSfzFile, "", kCenterText, 24); +view__7->addView(view__15); +EditFileButton* const view__16 = createEditFileButton(CRect(340, 9, 365, 34), kTagEditSfzFile, "", kCenterText, 24); +view__7->addView(view__16); +Label* const view__17 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); +infoVoicesLabel_ = view__17; +view__7->addView(view__17); +Label* const view__18 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); +view__7->addView(view__18); +Label* const view__19 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); +numVoicesLabel_ = view__19; +view__7->addView(view__19); +Label* const view__20 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); +view__7->addView(view__20); +Label* const view__21 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); +memoryLabel_ = view__21; +view__7->addView(view__21); +RoundedGroup* const view__22 = createRoundedGroup(CRect(570, 5, 795, 105), -1, "", kCenterText, 14); +view__1->addView(view__22); +Knob48* const view__23 = createKnob48(CRect(45, 15, 93, 63), -1, "", kCenterText, 14); +view__22->addView(view__23); +view__23->setVisible(false); +ValueLabel* const view__24 = createValueLabel(CRect(40, 65, 100, 70), -1, "Center", kCenterText, 12); +view__22->addView(view__24); view__24->setVisible(false); -ValueLabel* const view__25 = createValueLabel(CRect(10, 65, 70, 90), -1, "Center", kCenterText, 12); -view__23->addView(view__25); -view__25->setVisible(false); -Knob48* const view__26 = createKnob48(CRect(80, 15, 128, 63), kTagSetVolume, "", kCenterText, 14); -volumeSlider_ = view__26; -view__23->addView(view__26); -ValueLabel* const view__27 = createValueLabel(CRect(75, 65, 135, 90), -1, "0.0 dB", kCenterText, 12); -volumeLabel_ = view__27; -view__23->addView(view__27); -VMeter* const view__28 = createVMeter(CRect(145, 15, 180, 85), -1, "", kCenterText, 14); -view__23->addView(view__28); +Knob48* const view__25 = createKnob48(CRect(110, 15, 158, 63), kTagSetVolume, "", kCenterText, 14); +volumeSlider_ = view__25; +view__22->addView(view__25); +ValueLabel* const view__26 = createValueLabel(CRect(105, 65, 165, 87), -1, "0.0 dB", kCenterText, 12); +volumeLabel_ = view__26; +view__22->addView(view__26); +VMeter* const view__27 = createVMeter(CRect(175, 15, 210, 70), -1, "", kCenterText, 14); +view__22->addView(view__27); enterTheme(defaultTheme); -LogicalGroup* const view__29 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelGeneral] = view__29; -view__0->addView(view__29); -view__29->setVisible(false); -RoundedGroup* const view__30 = createRoundedGroup(CRect(0, 0, 120, 285), -1, "", kCenterText, 14); +LogicalGroup* const view__28 = createLogicalGroup(CRect(5, 110, 796, 395), -1, "", kCenterText, 14); +subPanels_[kPanelGeneral] = view__28; +view__0->addView(view__28); +RoundedGroup* const view__29 = createRoundedGroup(CRect(0, 0, 175, 280), -1, "", kCenterText, 14); +view__28->addView(view__29); +Label* const view__30 = createLabel(CRect(15, 10, 75, 35), -1, "Curves:", kLeftText, 14); view__29->addView(view__30); -Label* const view__31 = createLabel(CRect(10, 10, 70, 35), -1, "Curves:", kLeftText, 12); -view__30->addView(view__31); -Label* const view__32 = createLabel(CRect(10, 35, 70, 60), -1, "Masters:", kLeftText, 12); -view__30->addView(view__32); -Label* const view__33 = createLabel(CRect(10, 60, 70, 85), -1, "Groups:", kLeftText, 12); -view__30->addView(view__33); -Label* const view__34 = createLabel(CRect(10, 85, 70, 110), -1, "Regions:", kLeftText, 12); -view__30->addView(view__34); -Label* const view__35 = createLabel(CRect(10, 110, 70, 135), -1, "Samples:", kLeftText, 12); -view__30->addView(view__35); -Label* const view__36 = createLabel(CRect(70, 10, 110, 35), -1, "0", kCenterText, 12); -infoCurvesLabel_ = view__36; -view__30->addView(view__36); -Label* const view__37 = createLabel(CRect(70, 35, 110, 60), -1, "0", kCenterText, 12); -infoMastersLabel_ = view__37; -view__30->addView(view__37); -Label* const view__38 = createLabel(CRect(70, 60, 110, 85), -1, "0", kCenterText, 12); -infoGroupsLabel_ = view__38; -view__30->addView(view__38); -Label* const view__39 = createLabel(CRect(70, 85, 110, 110), -1, "0", kCenterText, 12); -infoRegionsLabel_ = view__39; -view__30->addView(view__39); -Label* const view__40 = createLabel(CRect(70, 110, 110, 135), -1, "0", kCenterText, 12); -infoSamplesLabel_ = view__40; -view__30->addView(view__40); -LogicalGroup* const view__41 = createLogicalGroup(CRect(125, 0, 790, 280), -1, "", kCenterText, 14); -view__29->addView(view__41); -SfizzLargePicture* const view__42 = createSfizzLargePicture(CRect(130, 15, 530, 265), -1, "", kCenterText, 14); +Label* const view__31 = createLabel(CRect(15, 35, 75, 60), -1, "Masters:", kLeftText, 14); +view__29->addView(view__31); +Label* const view__32 = createLabel(CRect(15, 60, 75, 85), -1, "Groups:", kLeftText, 14); +view__29->addView(view__32); +Label* const view__33 = createLabel(CRect(15, 85, 75, 110), -1, "Regions:", kLeftText, 14); +view__29->addView(view__33); +Label* const view__34 = createLabel(CRect(15, 110, 75, 135), -1, "Samples:", kLeftText, 14); +view__29->addView(view__34); +Label* const view__35 = createLabel(CRect(115, 10, 155, 35), -1, "0", kCenterText, 14); +infoCurvesLabel_ = view__35; +view__29->addView(view__35); +Label* const view__36 = createLabel(CRect(115, 35, 155, 60), -1, "0", kCenterText, 14); +infoMastersLabel_ = view__36; +view__29->addView(view__36); +Label* const view__37 = createLabel(CRect(115, 60, 155, 85), -1, "0", kCenterText, 14); +infoGroupsLabel_ = view__37; +view__29->addView(view__37); +Label* const view__38 = createLabel(CRect(115, 85, 155, 110), -1, "0", kCenterText, 14); +infoRegionsLabel_ = view__38; +view__29->addView(view__38); +Label* const view__39 = createLabel(CRect(115, 110, 155, 135), -1, "0", kCenterText, 14); +infoSamplesLabel_ = view__39; +view__29->addView(view__39); +LogicalGroup* const view__40 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelControls] = view__40; +view__0->addView(view__40); +view__40->setVisible(false); +RoundedGroup* const view__41 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); +view__40->addView(view__41); +Label* const view__42 = createLabel(CRect(0, 0, 790, 285), -1, "Controls not available", kCenterText, 40); view__41->addView(view__42); -LogicalGroup* const view__43 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelControls] = view__43; +LogicalGroup* const view__43 = createLogicalGroup(CRect(5, 109, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelSettings] = view__43; view__0->addView(view__43); view__43->setVisible(false); -RoundedGroup* const view__44 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); +TitleGroup* const view__44 = createTitleGroup(CRect(255, 1, 535, 111), -1, "Engine", kCenterText, 12); view__43->addView(view__44); -Label* const view__45 = createLabel(CRect(0, 0, 790, 285), -1, "Controls not available", kCenterText, 40); +ValueMenu* const view__45 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); +numVoicesSlider_ = view__45; view__44->addView(view__45); -LogicalGroup* const view__46 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelSettings] = view__46; -view__0->addView(view__46); -TitleGroup* const view__47 = createTitleGroup(CRect(255, 15, 535, 125), -1, "Engine", kCenterText, 12); -view__46->addView(view__47); -ValueMenu* const view__48 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); -numVoicesSlider_ = view__48; -view__47->addView(view__48); -ValueLabel* const view__49 = createValueLabel(CRect(15, 20, 95, 45), -1, "Polyphony", kCenterText, 12); -view__47->addView(view__49); -ValueMenu* const view__50 = createValueMenu(CRect(110, 60, 170, 85), kTagSetOversampling, "", kCenterText, 12); -oversamplingSlider_ = view__50; -view__47->addView(view__50); -ValueLabel* const view__51 = createValueLabel(CRect(100, 20, 180, 45), -1, "Oversampling", kCenterText, 12); -view__47->addView(view__51); -ValueLabel* const view__52 = createValueLabel(CRect(185, 20, 265, 45), -1, "Preload size", kCenterText, 12); -view__47->addView(view__52); -ValueMenu* const view__53 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); -preloadSizeSlider_ = view__53; -view__47->addView(view__53); -TitleGroup* const view__54 = createTitleGroup(CRect(200, 150, 590, 270), -1, "Tuning", kCenterText, 12); -view__46->addView(view__54); -ValueLabel* const view__55 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); -view__54->addView(view__55); -ValueMenu* const view__56 = createValueMenu(CRect(220, 60, 280, 85), kTagSetTuningFrequency, "", kCenterText, 12); -tuningFrequencySlider_ = view__56; -view__54->addView(view__56); -ValueLabel* const view__57 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); -view__54->addView(view__57); -Knob48* const view__58 = createKnob48(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); -stretchedTuningSlider_ = view__58; -view__54->addView(view__58); -ValueLabel* const view__59 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); -view__54->addView(view__59); -ValueLabel* const view__60 = createValueLabel(CRect(20, 20, 120, 45), -1, "Scala file", kCenterText, 12); -view__54->addView(view__60); -ValueButton* const view__61 = createValueButton(CRect(20, 60, 120, 85), kTagLoadScalaFile, "DefaultScale", kCenterText, 12); -scalaFileButton_ = view__61; -view__54->addView(view__61); -ValueMenu* const view__62 = createValueMenu(CRect(135, 60, 170, 85), kTagSetScalaRootKey, "", kCenterText, 12); -scalaRootKeySlider_ = view__62; -view__54->addView(view__62); -ValueMenu* const view__63 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); -scalaRootOctaveSlider_ = view__63; -view__54->addView(view__63); -LogicalGroup* const view__64 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelInfo] = view__64; -view__0->addView(view__64); -view__64->setVisible(false); -RoundedGroup* const view__65 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); -view__64->addView(view__65); -Label* const view__66 = createLabel(CRect(0, 0, 790, 285), -1, "Informative text goes here", kCenterText, 40); -view__65->addView(view__66); -Piano* const view__67 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); -view__0->addView(view__67); +ValueLabel* const view__46 = createValueLabel(CRect(15, 20, 95, 45), -1, "Polyphony", kCenterText, 12); +view__44->addView(view__46); +ValueMenu* const view__47 = createValueMenu(CRect(110, 60, 170, 85), kTagSetOversampling, "", kCenterText, 12); +oversamplingSlider_ = view__47; +view__44->addView(view__47); +ValueLabel* const view__48 = createValueLabel(CRect(100, 20, 180, 45), -1, "Oversampling", kCenterText, 12); +view__44->addView(view__48); +ValueLabel* const view__49 = createValueLabel(CRect(185, 20, 265, 45), -1, "Preload size", kCenterText, 12); +view__44->addView(view__49); +ValueMenu* const view__50 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); +preloadSizeSlider_ = view__50; +view__44->addView(view__50); +TitleGroup* const view__51 = createTitleGroup(CRect(200, 159, 590, 279), -1, "Tuning", kCenterText, 12); +view__43->addView(view__51); +ValueLabel* const view__52 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); +view__51->addView(view__52); +ValueMenu* const view__53 = createValueMenu(CRect(220, 60, 280, 85), kTagSetTuningFrequency, "", kCenterText, 12); +tuningFrequencySlider_ = view__53; +view__51->addView(view__53); +ValueLabel* const view__54 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); +view__51->addView(view__54); +Knob48* const view__55 = createKnob48(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); +stretchedTuningSlider_ = view__55; +view__51->addView(view__55); +ValueLabel* const view__56 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); +view__51->addView(view__56); +ValueLabel* const view__57 = createValueLabel(CRect(20, 20, 120, 45), -1, "Scala file", kCenterText, 12); +view__51->addView(view__57); +ValueButton* const view__58 = createValueButton(CRect(20, 60, 120, 85), kTagLoadScalaFile, "DefaultScale", kCenterText, 12); +scalaFileButton_ = view__58; +view__51->addView(view__58); +ValueMenu* const view__59 = createValueMenu(CRect(135, 60, 170, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootKeySlider_ = view__59; +view__51->addView(view__59); +ValueMenu* const view__60 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootOctaveSlider_ = view__60; +view__51->addView(view__60); +Piano* const view__61 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); +view__0->addView(view__61); From 574afcaf7435e54abafbe417cd9f7a66ee1b71cc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 10:21:48 +0200 Subject: [PATCH 222/445] Move HoverButton to GUIComponents --- editor/src/editor/Editor.cpp | 26 -------------------------- editor/src/editor/GUIComponents.cpp | 18 ++++++++++++++++++ editor/src/editor/GUIComponents.h | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 3d0c70ea..dc46b41a 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -313,32 +313,6 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) break; } } -class CHoverButton: public CTextButton { -public: - CHoverButton(const CRect& size, IControlListener* listener = nullptr, int32_t tag = -1, UTF8StringPtr title = nullptr) - : CTextButton(size, listener, tag, title) {} - - void setHoverColor(const CColor& color) - { - hoverColor = color; - } - - CMouseEventResult onMouseEntered (CPoint& where, const CButtonState& buttons) override - { - backupColor = getTextColor(); - setTextColor(hoverColor); - return CTextButton::onMouseEntered(where, buttons); - } - - CMouseEventResult onMouseExited (CPoint& where, const CButtonState& buttons) override - { - setTextColor(backupColor); - return CTextButton::onMouseExited(where, buttons); - } -private: - CColor hoverColor; - CColor backupColor; -}; void Editor::Impl::createFrameContents() { diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index d604d76e..cf344489 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -427,3 +427,21 @@ void SValueMenu::onItemClicked(int32_t index) if (getValue() != oldValue) valueChanged(); } + +void CHoverButton::setHoverColor (const CColor& color) +{ + hoverColor_ = color; +} + +CMouseEventResult CHoverButton::onMouseEntered (CPoint& where, const CButtonState& buttons) +{ + backupColor_ = getTextColor(); + setTextColor(hoverColor_); + return CTextButton::onMouseEntered(where, buttons); +} + +CMouseEventResult CHoverButton::onMouseExited (CPoint& where, const CButtonState& buttons) +{ + setTextColor(backupColor_); + return CTextButton::onMouseExited(where, buttons); +} diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index 8c295f5e..6b970864 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -11,6 +11,7 @@ #include "vstgui/lib/controls/cslider.h" #include "vstgui/lib/controls/cknob.h" #include "vstgui/lib/controls/ctextlabel.h" +#include "vstgui/lib/controls/cbuttons.h" #include "vstgui/lib/controls/coptionmenu.h" #include "vstgui/lib/cviewcontainer.h" #include "vstgui/lib/ccolor.h" @@ -146,3 +147,17 @@ private: SValueMenu& menu_; }; }; + +/// +class CHoverButton: public CTextButton { +public: + CHoverButton(const CRect& size, IControlListener* listener = nullptr, int32_t tag = -1, UTF8StringPtr title = nullptr) + : CTextButton(size, listener, tag, title) {} + + void setHoverColor(const CColor& color); + CMouseEventResult onMouseEntered (CPoint& where, const CButtonState& buttons) override; + CMouseEventResult onMouseExited (CPoint& where, const CButtonState& buttons) override; +private: + CColor hoverColor_; + CColor backupColor_; +}; From 65e75523bb0f2720dac8808856e0a13d3ead8ad4 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 12:52:19 +0200 Subject: [PATCH 223/445] Change button name --- editor/src/editor/Editor.cpp | 12 ++++++------ editor/src/editor/GUIComponents.cpp | 6 +++--- editor/src/editor/GUIComponents.h | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index dc46b41a..24503d1e 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -373,11 +373,11 @@ void Editor::Impl::createFrameContents() typedef CTextButton Button; #endif typedef CTextButton ValueButton; - typedef CHoverButton LoadFileButton; - typedef CHoverButton CCButton; - typedef CHoverButton HomeButton; - typedef CHoverButton SettingsButton; - typedef CHoverButton EditFileButton; + typedef SHoverButton LoadFileButton; + typedef SHoverButton CCButton; + typedef SHoverButton HomeButton; + typedef SHoverButton SettingsButton; + typedef SHoverButton EditFileButton; typedef SPiano Piano; auto createLogicalGroup = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { @@ -480,7 +480,7 @@ void Editor::Impl::createFrameContents() return vm; }; auto createGlyphButton = [this, &theme](UTF8StringPtr glyph, const CRect& bounds, int tag, int fontsize) { - CHoverButton* btn = new CHoverButton(bounds, this, tag, glyph); + SHoverButton* btn = new SHoverButton(bounds, this, tag, glyph); btn->setFont(new CFontDesc("Fluent System Regular W20", fontsize)); btn->setTextColor(theme->icon); btn->setHoverColor(theme->iconHighlight); diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index cf344489..84afec45 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -428,19 +428,19 @@ void SValueMenu::onItemClicked(int32_t index) valueChanged(); } -void CHoverButton::setHoverColor (const CColor& color) +void SHoverButton::setHoverColor (const CColor& color) { hoverColor_ = color; } -CMouseEventResult CHoverButton::onMouseEntered (CPoint& where, const CButtonState& buttons) +CMouseEventResult SHoverButton::onMouseEntered (CPoint& where, const CButtonState& buttons) { backupColor_ = getTextColor(); setTextColor(hoverColor_); return CTextButton::onMouseEntered(where, buttons); } -CMouseEventResult CHoverButton::onMouseExited (CPoint& where, const CButtonState& buttons) +CMouseEventResult SHoverButton::onMouseExited (CPoint& where, const CButtonState& buttons) { setTextColor(backupColor_); return CTextButton::onMouseExited(where, buttons); diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index 6b970864..31d14bae 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -149,9 +149,9 @@ private: }; /// -class CHoverButton: public CTextButton { +class SHoverButton: public CTextButton { public: - CHoverButton(const CRect& size, IControlListener* listener = nullptr, int32_t tag = -1, UTF8StringPtr title = nullptr) + SHoverButton(const CRect& size, IControlListener* listener = nullptr, int32_t tag = -1, UTF8StringPtr title = nullptr) : CTextButton(size, listener, tag, title) {} void setHoverColor(const CColor& color); From fa548a6193fcd45fe92c514583fa2ff873719676 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 13:19:23 +0200 Subject: [PATCH 224/445] Change font, top left icon, and engraving --- editor/CMakeLists.txt | 8 +- editor/layout/main.fl | 16 +- editor/resources/Fonts/ABeeZee-Italic.ttf | Bin 45780 -> 0 bytes editor/resources/Fonts/ABeeZee-Regular.ttf | Bin 44184 -> 0 bytes editor/resources/Fonts/Roboto-Regular.ttf | Bin 0 -> 171272 bytes editor/resources/background.png | Bin 0 -> 6312 bytes editor/resources/background@2x.png | Bin 0 -> 13771 bytes editor/resources/knob48.png | Bin 10955 -> 7382 bytes editor/resources/knob48@2x.png | Bin 109207 -> 92347 bytes editor/resources/logo_full_white.png | Bin 4516 -> 0 bytes editor/resources/logo_text_white.png | Bin 0 -> 2591 bytes editor/resources/logo_text_white@2x.png | Bin 0 -> 4056 bytes editor/src/editor/Editor.cpp | 19 +- editor/src/editor/layout/main.hpp | 288 +++++++++++---------- 14 files changed, 173 insertions(+), 158 deletions(-) delete mode 100644 editor/resources/Fonts/ABeeZee-Italic.ttf delete mode 100644 editor/resources/Fonts/ABeeZee-Regular.ttf create mode 100644 editor/resources/Fonts/Roboto-Regular.ttf create mode 100644 editor/resources/background.png create mode 100644 editor/resources/background@2x.png delete mode 100644 editor/resources/logo_full_white.png create mode 100644 editor/resources/logo_text_white.png create mode 100644 editor/resources/logo_text_white@2x.png diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 935f5c79..33f99afe 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4,15 +4,17 @@ include("cmake/Vstgui.cmake") set(EDITOR_RESOURCES logo.png logo_text.png + logo_text_white.png logo_text@2x.png - logo_full_white.png + logo_text_white@2x.png + background.png + background@2x.png icon_white.png icon_white@2x.png knob48.png knob48@2x.png Fonts/fluentui-system-regular-20.ttf - Fonts/ABeeZee-Regular.ttf - Fonts/ABeeZee-Regular.ttf + Fonts/Roboto-Regular.ttf PARENT_SCOPE) function(copy_editor_resources SOURCE_DIR DESTINATION_DIR) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index 477d2d40..3ad0e0f5 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -6,8 +6,12 @@ widget_class mainView {open xywh {572 266 800 475} type Double class LogicalGroup visible } { + Fl_Box {} {selected + image {../resources/background.png} xywh {190 110 600 280} + class Background + } Fl_Group {} { - comment {theme=darkTheme} open + comment {theme=darkTheme} xywh {0 0 800 110} class LogicalGroup } { @@ -17,22 +21,22 @@ widget_class mainView {open } { Fl_Box {} { comment {tag=kTagFirstChangePanel+kPanelGeneral} - image {../resources/logo_full_white.png} xywh {10 11 165 63} + image {../resources/logo_text_white.png} xywh {35 9 120 60} class SfizzMainButton } Fl_Button {} { comment {tag=kTagFirstChangePanel+kPanelGeneral} - xywh {55 75 25 25} labelsize 24 + xywh {49 73 25 25} labelsize 24 class HomeButton } Fl_Button {} { comment {tag=kTagFirstChangePanel+kPanelControls} - xywh {80 75 25 25} labelsize 24 + xywh {81 73 25 25} labelsize 24 class CCButton } Fl_Button {} { comment {tag=kTagFirstChangePanel+kPanelSettings} - xywh {105 75 25 25} labelsize 24 + xywh {112 73 25 25} labelsize 24 class SettingsButton } } @@ -137,7 +141,7 @@ widget_class mainView {open } } } - Fl_Group {subPanels_[kPanelGeneral]} {open selected + Fl_Group {subPanels_[kPanelGeneral]} { xywh {5 110 791 285} class LogicalGroup } { diff --git a/editor/resources/Fonts/ABeeZee-Italic.ttf b/editor/resources/Fonts/ABeeZee-Italic.ttf deleted file mode 100644 index d98743aff8bc049de88675e3c7efbfd89264d8f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45780 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 diff --git a/editor/resources/Fonts/ABeeZee-Regular.ttf b/editor/resources/Fonts/ABeeZee-Regular.ttf deleted file mode 100644 index 71e6d16245f37eb1e7aecd847f4c4ae54c9e836d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44184 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 @+H{4m&%{0J+I_`J9Vk_k+ss_AGuWe z=vwLR$Ja{5_O&nmj=LLQ7AYD^JOn$i6=S_|;D%vG_D8Xcy6H3@G_sF^qez%UaKYAq z8>PvaGdZyblNL%Ji{$dLKs5;IzvVJq39^S*8hdgiCE)jJCWX*^zL{Cv&F-it0LwxCLwR1sh_6sh+Vu{tHr+ zG5<%SrDIdMTZ%5Og&g;Hwhei5UD0%Z!L3z~Z8ICH?{%bm(pq(ECe_5>DaRle;31%> z_9^EPh+3?YRYds$e-?;W zAnh2hfN~{XsViUN?paq(yh6`|S2`{|O~q+ z*hZF)Jm*P*TlS=y zrvqYJVUv1bN)wpK`&yDNN4!ahYeMONj4o}kp2B3f~*vKNw_?UQj&XbXE@tfzQo_ z&jXhOmujK9rPAN;x>QP-kVW?N<4cxp@$jv zBw0`}iq1YzIFGfX;3F@%3+X~fVXQD)SSXw3$4`h23xo}_WLg;yU0K8Q9Soxz%a^!Q>&gk!^gJ*< zc&Qe`sZ@IVekzsXlZcA0VRbJ7%fxllurd;`hA0;FZIULd8{^i+Y%x1SHagbiD-!o! zIv)k$pe(}*;d%(3c&Q94hgHgpK%|aiLwxLuXIRxDWBE{hSnN;4<^NirU`27U{Qs^C zid};bL5<*}6HxNKmXv>CCOS z)@Mp=+cqb>Vep;L%a#})+zZUF{u~`ivr(2bA{AH9kUhQn7|O#;+DKCP_n)91Lk4jl z$K1x+`WVblD)XXjXiA9{NTv|ffE}&yN}hCiY}?1%7=8A@U-lK`R3y0rt0cJ&(mXAm zr`0qbXJh0(d`$&Qew>Ywk%Pf+0<@FdZ$Cmfz?#x{dU`cXc@MzMA$pp-$lZk}C?Dc4 zSd+^H9My7XS>xoE)|q2<@$<|!gE@vY36>?GW(OMkIxsA9sjvp(^xP92-~LSxdM2p< z^H&|;_({*jXQ-b55LUxLO9og=p5%T}eHi87PyQ9G-G^CuoO?T)!BBvDRDg>C@{JKl zU0SgW%N2oRYh0+=f%0eAS{fLYVS)uMLVl>KdT7Ld`N2p0zvBmZoqs6BUtazC?x90J zA&e0;$;a)X(F}(DWM#5|C+Z_-bd$_{86k#t_9PqDs%m~nc^RxW)|Ruc3f)(i(y=Lo zB%RN8Z65ply3~$$`!;9!J%l$v3!{7THATbtWq$CpP� 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 literal 0 HcmV?d00001 diff --git a/editor/resources/background.png b/editor/resources/background.png new file mode 100644 index 0000000000000000000000000000000000000000..671268679ba6d859bc9801740dbeb02e1a6defd8 GIT binary patch literal 6312 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 literal 0 HcmV?d00001 diff --git a/editor/resources/knob48.png b/editor/resources/knob48.png index aa5c8eb3946232f8029d0475805431dc7645e949..d0fc774ab215af5c5e0db00879e2a2fc4a03c8af 100644 GIT binary patch literal 7382 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 KH}m0l0{;)j%WALy literal 10955 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

BA(xO6jz4z{UdPwKcHf2dsn#I zVgFTCipRA@B&j(cWvXj3>8We0tcBt(9uUj=rTqQPKn98fniM*mTq`Cr$H z%!iEOZqpSe?;uLR-WQ+vN}pm}Tp&kmLsHMuad+PfG;TOo`(?o&?{s&!JzTKjyc7mdb^xhhq^xz^2EhES`P;pnv<@|)Xc*955dSxv)<5=fH|rovbVHd5BJ>~S(OEEr@fqs< z5~U@Y-v0ch(~j$kZT(>WsBwlq_h0Xkp9*TH@{$Owy1sUFuWIY*E(B0{2~C}0#K(g9 ziktu9=o)ng6|4vD4dCkFwYblE{)IIvIJ+gRqf8Elt932gQ&Ku zW&g|z7m163|ym0UMh5v>C?G2>jCDy0qo8OJX49 z{I9lYI_wDZc)Egq$EFn1+{Ok4mk5!0nYOzON92mY8O!BJg^Cs zcexp5FK3&35EYz0K=mi|;Z9u=02fSffcRBwCH<;GYN5 zgFTImJv!DofG>)@IS!2BEjciJmU_uEWV3(ds zG?_d-WK@5@{@bg|026Nwi{{FCWaTQHX+%sPsQM{Bo6uxSe`p9g`YS;K=!zLw^Ca-; zfI9k7^G3t&jq=`bzHYN>GyP_#34*Yl7C7|M%K(jM2f&@=>wmi2y@2hu3!Hy8OryBN zU+s6VdJ=zh^>q2YS`3;BJg3M%Hb&&Yd7a_ZAxVQ?(!SbEm+c>2rbz!&pcQR{(fv?9J_MV$BBr^|Rm zj{(uy`Sm=pBUNla2Z=uknyTjVUFTLdgtYO#e_zMCcM9+2wB?Yha|<*I1Yolf#h zGh}OpXifxp!4!UuY*p#xGz3Yb7 zfO~af1tRJ>Ii;1N7#`_cGAE2bv7Zv%2HoDwZ4c!c^NdtXO5L+ZwQHD}-&qNfL;yrl z)m_?=mcB!DhbJraZ=Q1h6(#=W!o6Pxjf#u!`^R&x9ut^V)1#m!R4;gv#0%cTujG14 z5q*l)#6dMM-FTN2kv*3q&uM4Io3*bFaL5Hx^lcV%ISX0)ah!@-#-@Oe%7Ad2(N=se z8Jfys<2>f6xp7+-(b-SB_;j}GtFJVT&`uqEvBGgO@nkj=%G%uf&RlMuTAaciZ8nbl z%P8yfBC_%lOmxyqMtc;n?Zzgdg%FSo=of0>QIOK;1(LDyXh?gap)S_l?2X7`(! zQJ>C&F8o&yvu_c3U>cmpz)>uIx7|O7NilA1xi4rb4;oR9c;K${m-F^1UP+ar^P*gM zVScw=(K(A?*BcfQP?JTNhp1A27a`G2iHri<55j93hkMr(EWr$%DcW3-yvcA%oB#~y8Ai|;^;1r{)10^R@y8#7veg@ChPEBKw?HQq%TsZeF_7`9WGecd!t3QHlenoaS?W-gz?9-b zPkby#%jG_m^9UUieZ-C9h9w`v(~sg?XK`kZnwvw>~iIFAsQkP&>U4CSf= zt*&8$cID4GXw!qae!o|un3SSPuWL^QRYA7*I)cWwPT(` jfRz8I^YP)-9mYGY$4Wv})hxh=RQIS{Q&Y-A-u3@KY&Vc| diff --git a/editor/resources/knob48@2x.png b/editor/resources/knob48@2x.png index 2f8d49e67d3ca65f4cf0ccc8ac447557fc08129f..f064cfc3e5ef86c2b48ba69c07c210ece9b15dca 100644 GIT binary patch literal 92347 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

jLW!z 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 diff --git a/editor/resources/logo_full_white.png b/editor/resources/logo_full_white.png deleted file mode 100644 index 8aace05a9f2fc2c1b5305d3d6687edce1bf3f552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4516 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 literal 0 HcmV?d00001 diff --git a/editor/resources/logo_text_white@2x.png b/editor/resources/logo_text_white@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d3464ae7d25283a215a2b59cf5e74922f50b5e10 GIT binary patch literal 4056 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 iconWhite = owned(new CBitmap("logo_full_white.png")); + SharedPointer iconWhite = owned(new CBitmap("logo_text_white.png")); + SharedPointer background = owned(new CBitmap("background.png")); SharedPointer knob48 = owned(new CBitmap("knob48.png")); SharedPointer logoText = owned(new CBitmap("logo_text.png")); @@ -369,6 +370,7 @@ void Editor::Impl::createFrameContents() typedef CTextLabel ValueLabel; typedef CViewContainer VMeter; typedef SValueMenu ValueMenu; + typedef CViewContainer Background; #if 0 typedef CTextButton Button; #endif @@ -397,7 +399,7 @@ void Editor::Impl::createFrameContents() box->setBackgroundColor(theme->boxBackground); box->setTitleFontColor(theme->titleBoxText); box->setTitleBackgroundColor(theme->titleBoxBackground); - auto font = owned(new CFontDesc("ABeeZee", fontsize)); + auto font = owned(new CFontDesc("Roboto", fontsize)); font->setSize(fontsize); box->setTitleFont(font); return box; @@ -411,7 +413,7 @@ void Editor::Impl::createFrameContents() lbl->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); lbl->setFontColor(theme->text); lbl->setHoriAlign(align); - auto font = owned(new CFontDesc("ABeeZee", fontsize)); + auto font = owned(new CFontDesc("Roboto", fontsize)); font->setSize(fontsize); lbl->setFont(font); return lbl; @@ -432,7 +434,7 @@ void Editor::Impl::createFrameContents() lbl->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); lbl->setFontColor(theme->text); lbl->setHoriAlign(align); - auto font = owned(new CFontDesc("ABeeZee", fontsize)); + auto font = owned(new CFontDesc("Roboto", fontsize)); font->setSize(fontsize); lbl->setFont(font); return lbl; @@ -455,7 +457,7 @@ void Editor::Impl::createFrameContents() #endif auto createValueButton = [this, &theme](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { CTextButton* button = new CTextButton(bounds, this, tag, label); - auto font = owned(new CFontDesc("ABeeZee", fontsize)); + auto font = owned(new CFontDesc("Roboto", fontsize)); font->setSize(fontsize); button->setFont(font); button->setTextAlignment(align); @@ -469,7 +471,7 @@ void Editor::Impl::createFrameContents() auto createValueMenu = [this, &theme](const CRect& bounds, int tag, const char*, CHoriTxtAlign align, int fontsize) { SValueMenu* vm = new SValueMenu(bounds, this, tag); vm->setHoriAlign(align); - auto font = owned(new CFontDesc("ABeeZee", fontsize)); + auto font = owned(new CFontDesc("Roboto", fontsize)); font->setSize(fontsize); vm->setFont(font); vm->setFontColor(theme->valueText); @@ -509,6 +511,11 @@ void Editor::Impl::createFrameContents() SPiano* piano = new SPiano(bounds); return piano; }; + auto createBackground = [&background](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + CViewContainer* container = new CViewContainer(bounds); + container->setBackground(background); + return container; + }; #include "layout/main.hpp" diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index c8e93a39..a91fc21c 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -1,153 +1,155 @@ /* This file is generated by the layout maker tool. */ LogicalGroup* const view__0 = createLogicalGroup(CRect(0, 0, 800, 475), -1, "", kCenterText, 14); mainView = view__0; -enterTheme(darkTheme); -LogicalGroup* const view__1 = createLogicalGroup(CRect(0, 0, 800, 110), -1, "", kCenterText, 14); +Background* const view__1 = createBackground(CRect(190, 110, 790, 390), -1, "", kCenterText, 14); view__0->addView(view__1); -RoundedGroup* const view__2 = createRoundedGroup(CRect(5, 4, 180, 105), -1, "", kCenterText, 14); -view__1->addView(view__2); -SfizzMainButton* const view__3 = createSfizzMainButton(CRect(5, 7, 170, 70), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 14); +enterTheme(darkTheme); +LogicalGroup* const view__2 = createLogicalGroup(CRect(0, 0, 800, 110), -1, "", kCenterText, 14); +view__0->addView(view__2); +RoundedGroup* const view__3 = createRoundedGroup(CRect(5, 4, 180, 105), -1, "", kCenterText, 14); view__2->addView(view__3); -HomeButton* const view__4 = createHomeButton(CRect(50, 71, 75, 96), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 24); -view__2->addView(view__4); -CCButton* const view__5 = createCCButton(CRect(75, 71, 100, 96), kTagFirstChangePanel+kPanelControls, "", kCenterText, 24); -view__2->addView(view__5); -SettingsButton* const view__6 = createSettingsButton(CRect(100, 71, 125, 96), kTagFirstChangePanel+kPanelSettings, "", kCenterText, 24); -view__2->addView(view__6); -RoundedGroup* const view__7 = createRoundedGroup(CRect(185, 5, 565, 105), -1, "", kCenterText, 14); -view__1->addView(view__7); -Label* const view__8 = createLabel(CRect(15, 8, 55, 38), -1, "File:", kCenterText, 16); -view__7->addView(view__8); -Label* const view__9 = createLabel(CRect(15, 40, 55, 70), -1, "KS:", kCenterText, 16); -view__7->addView(view__9); -HLine* const view__10 = createHLine(CRect(10, 36, 370, 41), -1, "", kCenterText, 14); -view__7->addView(view__10); -HLine* const view__11 = createHLine(CRect(10, 68, 370, 73), -1, "", kCenterText, 14); -view__7->addView(view__11); -Label* const view__12 = createLabel(CRect(80, 7, 310, 37), -1, "DefaultInstrument.sfz", kCenterText, 20); -sfzFileLabel_ = view__12; -view__7->addView(view__12); -Label* const view__13 = createLabel(CRect(80, 39, 310, 69), -1, "Key switch", kCenterText, 20); -view__7->addView(view__13); -Label* const view__14 = createLabel(CRect(10, 71, 70, 96), -1, "Voices:", kRightText, 12); -view__7->addView(view__14); -LoadFileButton* const view__15 = createLoadFileButton(CRect(315, 9, 340, 34), kTagLoadSfzFile, "", kCenterText, 24); -view__7->addView(view__15); -EditFileButton* const view__16 = createEditFileButton(CRect(340, 9, 365, 34), kTagEditSfzFile, "", kCenterText, 24); -view__7->addView(view__16); -Label* const view__17 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); -infoVoicesLabel_ = view__17; -view__7->addView(view__17); -Label* const view__18 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); -view__7->addView(view__18); -Label* const view__19 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); -numVoicesLabel_ = view__19; -view__7->addView(view__19); -Label* const view__20 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); -view__7->addView(view__20); -Label* const view__21 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); -memoryLabel_ = view__21; -view__7->addView(view__21); -RoundedGroup* const view__22 = createRoundedGroup(CRect(570, 5, 795, 105), -1, "", kCenterText, 14); -view__1->addView(view__22); -Knob48* const view__23 = createKnob48(CRect(45, 15, 93, 63), -1, "", kCenterText, 14); -view__22->addView(view__23); -view__23->setVisible(false); -ValueLabel* const view__24 = createValueLabel(CRect(40, 65, 100, 70), -1, "Center", kCenterText, 12); -view__22->addView(view__24); +SfizzMainButton* const view__4 = createSfizzMainButton(CRect(30, 5, 150, 65), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 14); +view__3->addView(view__4); +HomeButton* const view__5 = createHomeButton(CRect(44, 69, 69, 94), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 24); +view__3->addView(view__5); +CCButton* const view__6 = createCCButton(CRect(76, 69, 101, 94), kTagFirstChangePanel+kPanelControls, "", kCenterText, 24); +view__3->addView(view__6); +SettingsButton* const view__7 = createSettingsButton(CRect(107, 69, 132, 94), kTagFirstChangePanel+kPanelSettings, "", kCenterText, 24); +view__3->addView(view__7); +RoundedGroup* const view__8 = createRoundedGroup(CRect(185, 5, 565, 105), -1, "", kCenterText, 14); +view__2->addView(view__8); +Label* const view__9 = createLabel(CRect(15, 8, 55, 38), -1, "File:", kCenterText, 16); +view__8->addView(view__9); +Label* const view__10 = createLabel(CRect(15, 40, 55, 70), -1, "KS:", kCenterText, 16); +view__8->addView(view__10); +HLine* const view__11 = createHLine(CRect(10, 36, 370, 41), -1, "", kCenterText, 14); +view__8->addView(view__11); +HLine* const view__12 = createHLine(CRect(10, 68, 370, 73), -1, "", kCenterText, 14); +view__8->addView(view__12); +Label* const view__13 = createLabel(CRect(80, 7, 310, 37), -1, "DefaultInstrument.sfz", kCenterText, 20); +sfzFileLabel_ = view__13; +view__8->addView(view__13); +Label* const view__14 = createLabel(CRect(80, 39, 310, 69), -1, "Key switch", kCenterText, 20); +view__8->addView(view__14); +Label* const view__15 = createLabel(CRect(10, 71, 70, 96), -1, "Voices:", kRightText, 12); +view__8->addView(view__15); +LoadFileButton* const view__16 = createLoadFileButton(CRect(315, 9, 340, 34), kTagLoadSfzFile, "", kCenterText, 24); +view__8->addView(view__16); +EditFileButton* const view__17 = createEditFileButton(CRect(340, 9, 365, 34), kTagEditSfzFile, "", kCenterText, 24); +view__8->addView(view__17); +Label* const view__18 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); +infoVoicesLabel_ = view__18; +view__8->addView(view__18); +Label* const view__19 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); +view__8->addView(view__19); +Label* const view__20 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); +numVoicesLabel_ = view__20; +view__8->addView(view__20); +Label* const view__21 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); +view__8->addView(view__21); +Label* const view__22 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); +memoryLabel_ = view__22; +view__8->addView(view__22); +RoundedGroup* const view__23 = createRoundedGroup(CRect(570, 5, 795, 105), -1, "", kCenterText, 14); +view__2->addView(view__23); +Knob48* const view__24 = createKnob48(CRect(45, 15, 93, 63), -1, "", kCenterText, 14); +view__23->addView(view__24); view__24->setVisible(false); -Knob48* const view__25 = createKnob48(CRect(110, 15, 158, 63), kTagSetVolume, "", kCenterText, 14); -volumeSlider_ = view__25; -view__22->addView(view__25); -ValueLabel* const view__26 = createValueLabel(CRect(105, 65, 165, 87), -1, "0.0 dB", kCenterText, 12); -volumeLabel_ = view__26; -view__22->addView(view__26); -VMeter* const view__27 = createVMeter(CRect(175, 15, 210, 70), -1, "", kCenterText, 14); -view__22->addView(view__27); +ValueLabel* const view__25 = createValueLabel(CRect(40, 65, 100, 70), -1, "Center", kCenterText, 12); +view__23->addView(view__25); +view__25->setVisible(false); +Knob48* const view__26 = createKnob48(CRect(110, 15, 158, 63), kTagSetVolume, "", kCenterText, 14); +volumeSlider_ = view__26; +view__23->addView(view__26); +ValueLabel* const view__27 = createValueLabel(CRect(105, 65, 165, 87), -1, "0.0 dB", kCenterText, 12); +volumeLabel_ = view__27; +view__23->addView(view__27); +VMeter* const view__28 = createVMeter(CRect(175, 15, 210, 70), -1, "", kCenterText, 14); +view__23->addView(view__28); enterTheme(defaultTheme); -LogicalGroup* const view__28 = createLogicalGroup(CRect(5, 110, 796, 395), -1, "", kCenterText, 14); -subPanels_[kPanelGeneral] = view__28; -view__0->addView(view__28); -RoundedGroup* const view__29 = createRoundedGroup(CRect(0, 0, 175, 280), -1, "", kCenterText, 14); -view__28->addView(view__29); -Label* const view__30 = createLabel(CRect(15, 10, 75, 35), -1, "Curves:", kLeftText, 14); +LogicalGroup* const view__29 = createLogicalGroup(CRect(5, 110, 796, 395), -1, "", kCenterText, 14); +subPanels_[kPanelGeneral] = view__29; +view__0->addView(view__29); +RoundedGroup* const view__30 = createRoundedGroup(CRect(0, 0, 175, 280), -1, "", kCenterText, 14); view__29->addView(view__30); -Label* const view__31 = createLabel(CRect(15, 35, 75, 60), -1, "Masters:", kLeftText, 14); -view__29->addView(view__31); -Label* const view__32 = createLabel(CRect(15, 60, 75, 85), -1, "Groups:", kLeftText, 14); -view__29->addView(view__32); -Label* const view__33 = createLabel(CRect(15, 85, 75, 110), -1, "Regions:", kLeftText, 14); -view__29->addView(view__33); -Label* const view__34 = createLabel(CRect(15, 110, 75, 135), -1, "Samples:", kLeftText, 14); -view__29->addView(view__34); -Label* const view__35 = createLabel(CRect(115, 10, 155, 35), -1, "0", kCenterText, 14); -infoCurvesLabel_ = view__35; -view__29->addView(view__35); -Label* const view__36 = createLabel(CRect(115, 35, 155, 60), -1, "0", kCenterText, 14); -infoMastersLabel_ = view__36; -view__29->addView(view__36); -Label* const view__37 = createLabel(CRect(115, 60, 155, 85), -1, "0", kCenterText, 14); -infoGroupsLabel_ = view__37; -view__29->addView(view__37); -Label* const view__38 = createLabel(CRect(115, 85, 155, 110), -1, "0", kCenterText, 14); -infoRegionsLabel_ = view__38; -view__29->addView(view__38); -Label* const view__39 = createLabel(CRect(115, 110, 155, 135), -1, "0", kCenterText, 14); -infoSamplesLabel_ = view__39; -view__29->addView(view__39); -LogicalGroup* const view__40 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelControls] = view__40; -view__0->addView(view__40); -view__40->setVisible(false); -RoundedGroup* const view__41 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); -view__40->addView(view__41); -Label* const view__42 = createLabel(CRect(0, 0, 790, 285), -1, "Controls not available", kCenterText, 40); +Label* const view__31 = createLabel(CRect(15, 10, 75, 35), -1, "Curves:", kLeftText, 14); +view__30->addView(view__31); +Label* const view__32 = createLabel(CRect(15, 35, 75, 60), -1, "Masters:", kLeftText, 14); +view__30->addView(view__32); +Label* const view__33 = createLabel(CRect(15, 60, 75, 85), -1, "Groups:", kLeftText, 14); +view__30->addView(view__33); +Label* const view__34 = createLabel(CRect(15, 85, 75, 110), -1, "Regions:", kLeftText, 14); +view__30->addView(view__34); +Label* const view__35 = createLabel(CRect(15, 110, 75, 135), -1, "Samples:", kLeftText, 14); +view__30->addView(view__35); +Label* const view__36 = createLabel(CRect(115, 10, 155, 35), -1, "0", kCenterText, 14); +infoCurvesLabel_ = view__36; +view__30->addView(view__36); +Label* const view__37 = createLabel(CRect(115, 35, 155, 60), -1, "0", kCenterText, 14); +infoMastersLabel_ = view__37; +view__30->addView(view__37); +Label* const view__38 = createLabel(CRect(115, 60, 155, 85), -1, "0", kCenterText, 14); +infoGroupsLabel_ = view__38; +view__30->addView(view__38); +Label* const view__39 = createLabel(CRect(115, 85, 155, 110), -1, "0", kCenterText, 14); +infoRegionsLabel_ = view__39; +view__30->addView(view__39); +Label* const view__40 = createLabel(CRect(115, 110, 155, 135), -1, "0", kCenterText, 14); +infoSamplesLabel_ = view__40; +view__30->addView(view__40); +LogicalGroup* const view__41 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelControls] = view__41; +view__0->addView(view__41); +view__41->setVisible(false); +RoundedGroup* const view__42 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); view__41->addView(view__42); -LogicalGroup* const view__43 = createLogicalGroup(CRect(5, 109, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelSettings] = view__43; -view__0->addView(view__43); -view__43->setVisible(false); -TitleGroup* const view__44 = createTitleGroup(CRect(255, 1, 535, 111), -1, "Engine", kCenterText, 12); -view__43->addView(view__44); -ValueMenu* const view__45 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); -numVoicesSlider_ = view__45; +Label* const view__43 = createLabel(CRect(0, 0, 790, 285), -1, "Controls not available", kCenterText, 40); +view__42->addView(view__43); +LogicalGroup* const view__44 = createLogicalGroup(CRect(5, 109, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelSettings] = view__44; +view__0->addView(view__44); +view__44->setVisible(false); +TitleGroup* const view__45 = createTitleGroup(CRect(255, 1, 535, 111), -1, "Engine", kCenterText, 12); view__44->addView(view__45); -ValueLabel* const view__46 = createValueLabel(CRect(15, 20, 95, 45), -1, "Polyphony", kCenterText, 12); -view__44->addView(view__46); -ValueMenu* const view__47 = createValueMenu(CRect(110, 60, 170, 85), kTagSetOversampling, "", kCenterText, 12); -oversamplingSlider_ = view__47; -view__44->addView(view__47); -ValueLabel* const view__48 = createValueLabel(CRect(100, 20, 180, 45), -1, "Oversampling", kCenterText, 12); -view__44->addView(view__48); -ValueLabel* const view__49 = createValueLabel(CRect(185, 20, 265, 45), -1, "Preload size", kCenterText, 12); -view__44->addView(view__49); -ValueMenu* const view__50 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); -preloadSizeSlider_ = view__50; -view__44->addView(view__50); -TitleGroup* const view__51 = createTitleGroup(CRect(200, 159, 590, 279), -1, "Tuning", kCenterText, 12); -view__43->addView(view__51); -ValueLabel* const view__52 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); -view__51->addView(view__52); -ValueMenu* const view__53 = createValueMenu(CRect(220, 60, 280, 85), kTagSetTuningFrequency, "", kCenterText, 12); -tuningFrequencySlider_ = view__53; -view__51->addView(view__53); -ValueLabel* const view__54 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); -view__51->addView(view__54); -Knob48* const view__55 = createKnob48(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); -stretchedTuningSlider_ = view__55; -view__51->addView(view__55); -ValueLabel* const view__56 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); -view__51->addView(view__56); -ValueLabel* const view__57 = createValueLabel(CRect(20, 20, 120, 45), -1, "Scala file", kCenterText, 12); -view__51->addView(view__57); -ValueButton* const view__58 = createValueButton(CRect(20, 60, 120, 85), kTagLoadScalaFile, "DefaultScale", kCenterText, 12); -scalaFileButton_ = view__58; -view__51->addView(view__58); -ValueMenu* const view__59 = createValueMenu(CRect(135, 60, 170, 85), kTagSetScalaRootKey, "", kCenterText, 12); -scalaRootKeySlider_ = view__59; -view__51->addView(view__59); -ValueMenu* const view__60 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); -scalaRootOctaveSlider_ = view__60; -view__51->addView(view__60); -Piano* const view__61 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); -view__0->addView(view__61); +ValueMenu* const view__46 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); +numVoicesSlider_ = view__46; +view__45->addView(view__46); +ValueLabel* const view__47 = createValueLabel(CRect(15, 20, 95, 45), -1, "Polyphony", kCenterText, 12); +view__45->addView(view__47); +ValueMenu* const view__48 = createValueMenu(CRect(110, 60, 170, 85), kTagSetOversampling, "", kCenterText, 12); +oversamplingSlider_ = view__48; +view__45->addView(view__48); +ValueLabel* const view__49 = createValueLabel(CRect(100, 20, 180, 45), -1, "Oversampling", kCenterText, 12); +view__45->addView(view__49); +ValueLabel* const view__50 = createValueLabel(CRect(185, 20, 265, 45), -1, "Preload size", kCenterText, 12); +view__45->addView(view__50); +ValueMenu* const view__51 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); +preloadSizeSlider_ = view__51; +view__45->addView(view__51); +TitleGroup* const view__52 = createTitleGroup(CRect(200, 159, 590, 279), -1, "Tuning", kCenterText, 12); +view__44->addView(view__52); +ValueLabel* const view__53 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); +view__52->addView(view__53); +ValueMenu* const view__54 = createValueMenu(CRect(220, 60, 280, 85), kTagSetTuningFrequency, "", kCenterText, 12); +tuningFrequencySlider_ = view__54; +view__52->addView(view__54); +ValueLabel* const view__55 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); +view__52->addView(view__55); +Knob48* const view__56 = createKnob48(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); +stretchedTuningSlider_ = view__56; +view__52->addView(view__56); +ValueLabel* const view__57 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); +view__52->addView(view__57); +ValueLabel* const view__58 = createValueLabel(CRect(20, 20, 120, 45), -1, "Scala file", kCenterText, 12); +view__52->addView(view__58); +ValueButton* const view__59 = createValueButton(CRect(20, 60, 120, 85), kTagLoadScalaFile, "DefaultScale", kCenterText, 12); +scalaFileButton_ = view__59; +view__52->addView(view__59); +ValueMenu* const view__60 = createValueMenu(CRect(135, 60, 170, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootKeySlider_ = view__60; +view__52->addView(view__60); +ValueMenu* const view__61 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootOctaveSlider_ = view__61; +view__52->addView(view__61); +Piano* const view__62 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); +view__0->addView(view__62); From 1ea6918cd5bd76ea738920855efba41b93ecf8bd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 13:26:26 +0200 Subject: [PATCH 225/445] Update the button --- editor/src/editor/Editor.cpp | 12 ++++++------ editor/src/editor/GUIComponents.cpp | 25 +++++++++++++++++++------ editor/src/editor/GUIComponents.h | 6 ++++-- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index dd0f21ab..256f6302 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -375,11 +375,11 @@ void Editor::Impl::createFrameContents() typedef CTextButton Button; #endif typedef CTextButton ValueButton; - typedef SHoverButton LoadFileButton; - typedef SHoverButton CCButton; - typedef SHoverButton HomeButton; - typedef SHoverButton SettingsButton; - typedef SHoverButton EditFileButton; + typedef STextButton LoadFileButton; + typedef STextButton CCButton; + typedef STextButton HomeButton; + typedef STextButton SettingsButton; + typedef STextButton EditFileButton; typedef SPiano Piano; auto createLogicalGroup = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { @@ -482,7 +482,7 @@ void Editor::Impl::createFrameContents() return vm; }; auto createGlyphButton = [this, &theme](UTF8StringPtr glyph, const CRect& bounds, int tag, int fontsize) { - SHoverButton* btn = new SHoverButton(bounds, this, tag, glyph); + STextButton* btn = new STextButton(bounds, this, tag, glyph); btn->setFont(new CFontDesc("Fluent System Regular W20", fontsize)); btn->setTextColor(theme->icon); btn->setHoverColor(theme->iconHighlight); diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index 84afec45..0ebcc7d9 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -428,20 +428,33 @@ void SValueMenu::onItemClicked(int32_t index) valueChanged(); } -void SHoverButton::setHoverColor (const CColor& color) +void STextButton::setHoverColor (const CColor& color) { hoverColor_ = color; } -CMouseEventResult SHoverButton::onMouseEntered (CPoint& where, const CButtonState& buttons) +void STextButton::draw(CDrawContext* context) { - backupColor_ = getTextColor(); - setTextColor(hoverColor_); + backupColor_ = textColor; + if (hovered) { + textColor = hoverColor_; // textColor is protected + } + CTextButton::draw(context); + if (hovered) + textColor = backupColor_; +} + + +CMouseEventResult STextButton::onMouseEntered (CPoint& where, const CButtonState& buttons) +{ + hovered = true; + setDirty(); return CTextButton::onMouseEntered(where, buttons); } -CMouseEventResult SHoverButton::onMouseExited (CPoint& where, const CButtonState& buttons) +CMouseEventResult STextButton::onMouseExited (CPoint& where, const CButtonState& buttons) { - setTextColor(backupColor_); + hovered = false; + setDirty(); return CTextButton::onMouseExited(where, buttons); } diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index 31d14bae..83d7d04a 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -149,15 +149,17 @@ private: }; /// -class SHoverButton: public CTextButton { +class STextButton: public CTextButton { public: - SHoverButton(const CRect& size, IControlListener* listener = nullptr, int32_t tag = -1, UTF8StringPtr title = nullptr) + STextButton(const CRect& size, IControlListener* listener = nullptr, int32_t tag = -1, UTF8StringPtr title = nullptr) : CTextButton(size, listener, tag, title) {} void setHoverColor(const CColor& color); CMouseEventResult onMouseEntered (CPoint& where, const CButtonState& buttons) override; CMouseEventResult onMouseExited (CPoint& where, const CButtonState& buttons) override; + void draw(CDrawContext* context) override; private: CColor hoverColor_; CColor backupColor_; + bool hovered { false }; }; From 3c607046c21c14986fc6396f87865d8d340aaccf Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 13:31:33 +0200 Subject: [PATCH 226/445] Change modify file glyph --- editor/src/editor/Editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 256f6302..d13db043 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -502,7 +502,7 @@ void Editor::Impl::createFrameContents() return createGlyphButton(u8"\ue2e4", bounds, tag, fontsize); }; auto createEditFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { - return createGlyphButton(u8"\ue142", bounds, tag, fontsize); + return createGlyphButton(u8"\ue148", bounds, tag, fontsize); }; auto createLoadFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { return createGlyphButton(u8"\ue1a3", bounds, tag, fontsize); From be56de72c19c57f2f28bff28c857c829a4538ab8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 14:02:14 +0200 Subject: [PATCH 227/445] Update vstgui for the new filedialog --- editor/external/vstgui4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index 7316f9fb..10417247 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit 7316f9fb2891b9f0e2a408cfbf4ec35aeec268bd +Subproject commit 104172476111a255a87d9853a4b6502071130b9d From c6f7e81b70bd4cb307db3f4211d3a955e78d8a05 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 14:05:26 +0200 Subject: [PATCH 228/445] STextButton: backup color as a local variable --- editor/src/editor/GUIComponents.cpp | 4 ++-- editor/src/editor/GUIComponents.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index 0ebcc7d9..be9899ef 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -435,13 +435,13 @@ void STextButton::setHoverColor (const CColor& color) void STextButton::draw(CDrawContext* context) { - backupColor_ = textColor; + CColor backupColor = textColor; if (hovered) { textColor = hoverColor_; // textColor is protected } CTextButton::draw(context); if (hovered) - textColor = backupColor_; + textColor = backupColor; } diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index 83d7d04a..e0f3b173 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -160,6 +160,5 @@ public: void draw(CDrawContext* context) override; private: CColor hoverColor_; - CColor backupColor_; bool hovered { false }; }; From e8f967c4625f40d6ea8db584e537834edbf4458d Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 15:25:18 +0200 Subject: [PATCH 229/445] Eliminate a redundancy --- editor/src/editor/Editor.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index d13db043..752601f0 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -400,7 +400,6 @@ void Editor::Impl::createFrameContents() box->setTitleFontColor(theme->titleBoxText); box->setTitleBackgroundColor(theme->titleBoxBackground); auto font = owned(new CFontDesc("Roboto", fontsize)); - font->setSize(fontsize); box->setTitleFont(font); return box; }; @@ -414,7 +413,6 @@ void Editor::Impl::createFrameContents() lbl->setFontColor(theme->text); lbl->setHoriAlign(align); auto font = owned(new CFontDesc("Roboto", fontsize)); - font->setSize(fontsize); lbl->setFont(font); return lbl; }; @@ -435,7 +433,6 @@ void Editor::Impl::createFrameContents() lbl->setFontColor(theme->text); lbl->setHoriAlign(align); auto font = owned(new CFontDesc("Roboto", fontsize)); - font->setSize(fontsize); lbl->setFont(font); return lbl; }; @@ -448,8 +445,7 @@ void Editor::Impl::createFrameContents() #if 0 auto createButton = [this](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { CTextButton* button = new CTextButton(bounds, this, tag, label); - auto font = owned(new CFontDesc(*button->getFont())); - font->setSize(fontsize); + auto font = owned(new CFontDesc("Roboto", fontsize)); button->setFont(font); button->setTextAlignment(align); return button; @@ -458,7 +454,6 @@ void Editor::Impl::createFrameContents() auto createValueButton = [this, &theme](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { CTextButton* button = new CTextButton(bounds, this, tag, label); auto font = owned(new CFontDesc("Roboto", fontsize)); - font->setSize(fontsize); button->setFont(font); button->setTextAlignment(align); button->setTextColor(theme->valueText); @@ -472,7 +467,6 @@ void Editor::Impl::createFrameContents() SValueMenu* vm = new SValueMenu(bounds, this, tag); vm->setHoriAlign(align); auto font = owned(new CFontDesc("Roboto", fontsize)); - font->setSize(fontsize); vm->setFont(font); vm->setFontColor(theme->valueText); vm->setBackColor(theme->valueBackground); From 24c0ec27801037fc531e065f00c5e4e114331862 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 15:29:52 +0200 Subject: [PATCH 230/445] Adjust sizes and positions of settings boxes --- editor/layout/main.fl | 46 +++++++++++++++---------------- editor/src/editor/layout/main.hpp | 6 ++-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index 3ad0e0f5..30e3488a 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -1,12 +1,12 @@ # data file for the Fltk User Interface Designer (fluid) -version 1.0304 +version 1.0305 header_name {.h} code_name {.cxx} widget_class mainView {open xywh {572 266 800 475} type Double class LogicalGroup visible } { - Fl_Box {} {selected + Fl_Box {} { image {../resources/background.png} xywh {190 110 600 280} class Background } @@ -142,7 +142,7 @@ widget_class mainView {open } } Fl_Group {subPanels_[kPanelGeneral]} { - xywh {5 110 791 285} + xywh {5 110 791 285} hide class LogicalGroup } { Fl_Group {} {open @@ -216,95 +216,95 @@ widget_class mainView {open } } } - Fl_Group {subPanels_[kPanelSettings]} { - xywh {5 109 790 286} hide + Fl_Group {subPanels_[kPanelSettings]} {open + xywh {5 109 790 286} class LogicalGroup } { Fl_Group {} { - label Engine open - xywh {260 110 280 110} box ROUNDED_BOX labelsize 12 align 17 + label Engine open selected + xywh {260 135 280 100} box ROUNDED_BOX labelsize 12 align 17 class TitleGroup } { Fl_Spinner numVoicesSlider_ { comment {tag=kTagSetNumVoices} - xywh {285 170 60 25} labelsize 12 textsize 12 + xywh {285 195 60 25} labelsize 12 textsize 12 class ValueMenu } Fl_Box {} { label Polyphony - xywh {275 130 80 25} labelsize 12 + xywh {275 155 80 25} labelsize 12 class ValueLabel } Fl_Spinner oversamplingSlider_ { comment {tag=kTagSetOversampling} - xywh {370 170 60 25} labelsize 12 textsize 12 + xywh {370 195 60 25} labelsize 12 textsize 12 class ValueMenu } Fl_Box {} { label Oversampling - xywh {360 130 80 25} labelsize 12 + xywh {360 155 80 25} labelsize 12 class ValueLabel } Fl_Box {} { label {Preload size} - xywh {445 130 80 25} labelsize 12 + xywh {445 155 80 25} labelsize 12 class ValueLabel } Fl_Spinner preloadSizeSlider_ { comment {tag=kTagSetPreloadSize} - xywh {455 170 60 25} labelsize 12 textsize 12 + xywh {455 195 60 25} labelsize 12 textsize 12 class ValueMenu } } Fl_Group {} { label Tuning open - xywh {205 268 390 120} box ROUNDED_BOX labelsize 12 align 17 + xywh {205 270 390 100} box ROUNDED_BOX labelsize 12 align 17 class TitleGroup } { Fl_Box {} { label {Root key} - xywh {330 288 80 25} labelsize 12 + xywh {330 290 80 25} labelsize 12 class ValueLabel } Fl_Spinner tuningFrequencySlider_ { comment {tag=kTagSetTuningFrequency} - xywh {425 328 60 25} labelsize 12 textsize 12 + xywh {425 330 60 25} labelsize 12 textsize 12 class ValueMenu } Fl_Box {} { label Frequency - xywh {415 288 80 25} labelsize 12 + xywh {415 290 80 25} labelsize 12 class ValueLabel } Fl_Dial stretchedTuningSlider_ { comment {tag=kTagSetStretchedTuning} - xywh {515 313 48 48} value 0.5 + xywh {515 315 48 48} value 0.5 class Knob48 } Fl_Box {} { label Stretch - xywh {500 288 80 25} labelsize 12 + xywh {500 290 80 25} labelsize 12 class ValueLabel } Fl_Box {} { label {Scala file} - xywh {225 288 100 25} labelsize 12 + xywh {225 290 100 25} labelsize 12 class ValueLabel } Fl_Button scalaFileButton_ { label DefaultScale comment {tag=kTagLoadScalaFile} - xywh {225 328 100 25} labelsize 12 + xywh {225 330 100 25} labelsize 12 class ValueButton } Fl_Spinner scalaRootKeySlider_ { comment {tag=kTagSetScalaRootKey} - xywh {340 328 35 25} labelsize 12 textsize 12 + xywh {340 330 35 25} labelsize 12 textsize 12 class ValueMenu } Fl_Spinner scalaRootOctaveSlider_ { comment {tag=kTagSetScalaRootKey} - xywh {375 328 30 25} labelsize 12 textsize 12 + xywh {375 330 30 25} labelsize 12 textsize 12 class ValueMenu } } diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index a91fc21c..18a413a5 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -70,6 +70,7 @@ enterTheme(defaultTheme); LogicalGroup* const view__29 = createLogicalGroup(CRect(5, 110, 796, 395), -1, "", kCenterText, 14); subPanels_[kPanelGeneral] = view__29; view__0->addView(view__29); +view__29->setVisible(false); RoundedGroup* const view__30 = createRoundedGroup(CRect(0, 0, 175, 280), -1, "", kCenterText, 14); view__29->addView(view__30); Label* const view__31 = createLabel(CRect(15, 10, 75, 35), -1, "Curves:", kLeftText, 14); @@ -108,8 +109,7 @@ view__42->addView(view__43); LogicalGroup* const view__44 = createLogicalGroup(CRect(5, 109, 795, 395), -1, "", kCenterText, 14); subPanels_[kPanelSettings] = view__44; view__0->addView(view__44); -view__44->setVisible(false); -TitleGroup* const view__45 = createTitleGroup(CRect(255, 1, 535, 111), -1, "Engine", kCenterText, 12); +TitleGroup* const view__45 = createTitleGroup(CRect(255, 26, 535, 126), -1, "Engine", kCenterText, 12); view__44->addView(view__45); ValueMenu* const view__46 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); numVoicesSlider_ = view__46; @@ -126,7 +126,7 @@ view__45->addView(view__50); ValueMenu* const view__51 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); preloadSizeSlider_ = view__51; view__45->addView(view__51); -TitleGroup* const view__52 = createTitleGroup(CRect(200, 159, 590, 279), -1, "Tuning", kCenterText, 12); +TitleGroup* const view__52 = createTitleGroup(CRect(200, 161, 590, 261), -1, "Tuning", kCenterText, 12); view__44->addView(view__52); ValueLabel* const view__53 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); view__52->addView(view__53); From f9cf9b35e6e8367dd3174d727eae1aaf8bccd10d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 12:27:39 +0200 Subject: [PATCH 231/445] Move NumericId into utility --- src/sfizz/Region.h | 2 +- src/sfizz/Voice.h | 2 +- src/sfizz/modulations/ModGenerator.h | 2 +- src/sfizz/modulations/ModKey.h | 2 +- src/sfizz/modulations/ModMatrix.h | 2 +- src/sfizz/{ => utility}/NumericId.h | 0 tests/SynthT.cpp | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename src/sfizz/{ => utility}/NumericId.h (100%) diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index e5fbb6c0..4635f78c 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -17,7 +17,7 @@ #include "AudioBuffer.h" #include "MidiState.h" #include "FileId.h" -#include "NumericId.h" +#include "utility/NumericId.h" #include "modulations/ModKey.h" #include "absl/types/optional.h" #include "absl/strings/string_view.h" diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index a010aaa8..c8ebb0c8 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -17,7 +17,7 @@ #include "LeakDetector.h" #include "OnePoleFilter.h" #include "PowerFollower.h" -#include "NumericId.h" +#include "utility/NumericId.h" #include "absl/types/span.h" #include #include diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h index 3d7aed12..7bb6c329 100644 --- a/src/sfizz/modulations/ModGenerator.h +++ b/src/sfizz/modulations/ModGenerator.h @@ -5,7 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once -#include "../NumericId.h" +#include "utility/NumericId.h" #include #include diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 5fff0197..aec4f5ea 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -7,7 +7,7 @@ #pragma once #include "ModKeyHash.h" #include "ModId.h" -#include "../NumericId.h" +#include "../utility/NumericId.h" #include #include diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 211cf06f..b0f9f58f 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -5,7 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once -#include "../NumericId.h" +#include "../utility/NumericId.h" #include #include #include diff --git a/src/sfizz/NumericId.h b/src/sfizz/utility/NumericId.h similarity index 100% rename from src/sfizz/NumericId.h rename to src/sfizz/utility/NumericId.h diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 8af5f4be..2144426a 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -7,7 +7,7 @@ #include "sfizz/Synth.h" #include "sfizz/SisterVoiceRing.h" #include "sfizz/SfzHelpers.h" -#include "sfizz/NumericId.h" +#include "sfizz/utility/NumericId.h" #include "TestHelpers.h" #include #include "catch2/catch.hpp" From e3fa56452cec4d01add6b28d6c30e3b4f3102783 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 12:47:07 +0200 Subject: [PATCH 232/445] Add headers to CMake: NumericId --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0837e2ac..ccba873a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ set (SFIZZ_HEADERS sfizz/Config.h sfizz/Curve.h sfizz/Debug.h + sfizz/utility/NumericId.h sfizz/utility/SpinMutex.h sfizz/utility/SpinMutex.cpp sfizz/modulations/ModId.h @@ -76,7 +77,6 @@ set (SFIZZ_HEADERS sfizz/MathHelpers.h sfizz/MidiState.h sfizz/ModifierHelpers.h - sfizz/NumericId.h sfizz/OnePoleFilter.h sfizz/Oversampler.h sfizz/Panning.h From 9d26b5742ec6bc97c018748ecb21d678dc676415 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 11 Sep 2020 12:47:07 +0200 Subject: [PATCH 233/445] Move SpinMutex.cpp in the source list --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ccba873a..6083c110 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,7 +29,6 @@ set (SFIZZ_HEADERS sfizz/Debug.h sfizz/utility/NumericId.h sfizz/utility/SpinMutex.h - sfizz/utility/SpinMutex.cpp sfizz/modulations/ModId.h sfizz/modulations/ModKey.h sfizz/modulations/ModKeyHash.h @@ -146,6 +145,7 @@ set (SFIZZ_SOURCES sfizz/modulations/ModMatrix.cpp sfizz/modulations/sources/Controller.cpp sfizz/modulations/sources/LFO.cpp + sfizz/utility/SpinMutex.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp From 60eecba2264904864b37a9e0f62c521123c83a67 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 17:16:31 +0200 Subject: [PATCH 234/445] Make NumericId hashable --- src/sfizz/utility/NumericId.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/sfizz/utility/NumericId.h b/src/sfizz/utility/NumericId.h index 5abd906f..73d0b12f 100644 --- a/src/sfizz/utility/NumericId.h +++ b/src/sfizz/utility/NumericId.h @@ -5,6 +5,8 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include "../StringViewHelpers.h" +#include /** * @brief Numeric identifier @@ -50,3 +52,12 @@ struct NumericId { private: int number_ = -1; }; + +namespace std { + template struct hash> { + size_t operator()(const NumericId &id) const + { + return hashNumber(id.number()); + } + }; +} From 11d5abe435e7ec7a90673fb36bbe5540c7f00a97 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 10 Sep 2020 01:00:44 +0200 Subject: [PATCH 235/445] Precompute targets for the modulation on voice start --- src/sfizz/Voice.cpp | 33 +++++++++++++++++++-------------- src/sfizz/Voice.h | 13 +++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 8647de86..b19c6cb5 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -144,6 +144,7 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate); resources.modMatrix.initVoice(id, region->getId(), delay); + saveModulationTargets(region); } int sfz::Voice::getCurrentSampleQuality() const noexcept @@ -370,22 +371,20 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept const auto numSamples = modulationSpan.size(); ModMatrix& mm = resources.modMatrix; - const ModKey volumeKey = ModKey::createNXYZ(ModId::Volume, region->getId()); - const ModKey amplitudeKey = ModKey::createNXYZ(ModId::Amplitude, region->getId()); // AmpEG envelope egEnvelope.getBlock(modulationSpan); // Amplitude envelope applyGain1(baseGain, modulationSpan); - if (float* mod = mm.getModulationByKey(amplitudeKey)) { + if (float* mod = mm.getModulation(amplitudeTarget)) { for (size_t i = 0; i < numSamples; ++i) modulationSpan[i] *= normalizePercents(mod[i]); } // Volume envelope applyGain1(db2mag(baseVolumedB), modulationSpan); - if (float* mod = mm.getModulationByKey(volumeKey)) { + if (float* mod = mm.getModulation(volumeTarget)) { for (size_t i = 0; i < numSamples; ++i) modulationSpan[i] *= db2mag(mod[i]); } @@ -437,14 +436,13 @@ void sfz::Voice::panStageMono(AudioSpan buffer) noexcept return; ModMatrix& mm = resources.modMatrix; - const ModKey panKey = ModKey::createNXYZ(ModId::Pan, region->getId()); // Prepare for stereo output copy(leftBuffer, rightBuffer); // Apply panning fill(*modulationSpan, region->pan); - if (float* mod = mm.getModulationByKey(panKey)) { + if (float* mod = mm.getModulation(panTarget)) { for (size_t i = 0; i < numSamples; ++i) (*modulationSpan)[i] += normalizePercents(mod[i]); } @@ -463,13 +461,10 @@ void sfz::Voice::panStageStereo(AudioSpan buffer) noexcept return; ModMatrix& mm = resources.modMatrix; - const ModKey panKey = ModKey::createNXYZ(ModId::Pan, region->getId()); - const ModKey widthKey = ModKey::createNXYZ(ModId::Width, region->getId()); - const ModKey positionKey = ModKey::createNXYZ(ModId::Position, region->getId()); // Apply panning fill(*modulationSpan, region->pan); - if (float* mod = mm.getModulationByKey(panKey)) { + if (float* mod = mm.getModulation(panTarget)) { for (size_t i = 0; i < numSamples; ++i) (*modulationSpan)[i] += normalizePercents(mod[i]); } @@ -477,14 +472,14 @@ void sfz::Voice::panStageStereo(AudioSpan buffer) noexcept // Apply the width/position process fill(*modulationSpan, region->width); - if (float* mod = mm.getModulationByKey(widthKey)) { + if (float* mod = mm.getModulation(widthTarget)) { for (size_t i = 0; i < numSamples; ++i) (*modulationSpan)[i] += normalizePercents(mod[i]); } width(*modulationSpan, leftBuffer, rightBuffer); fill(*modulationSpan, region->position); - if (float* mod = mm.getModulationByKey(positionKey)) { + if (float* mod = mm.getModulation(positionTarget)) { for (size_t i = 0; i < numSamples; ++i) (*modulationSpan)[i] += normalizePercents(mod[i]); } @@ -889,9 +884,8 @@ void sfz::Voice::pitchEnvelope(absl::Span pitchSpan) noexcept applyGain(*bends, pitchSpan); ModMatrix& mm = resources.modMatrix; - const ModKey pitchKey = ModKey::createNXYZ(ModId::Pitch, region->getId()); - if (float* mod = mm.getModulationByKey(pitchKey)) { + if (float* mod = mm.getModulation(pitchTarget)) { for (size_t i = 0; i < numFrames; ++i) pitchSpan[i] *= centsFactor(mod[i]); } @@ -902,3 +896,14 @@ void sfz::Voice::resetSmoothers() noexcept bendSmoother.reset(1.0f); gainSmoother.reset(0.0f); } + +void sfz::Voice::saveModulationTargets(const Region* region) noexcept +{ + ModMatrix& mm = resources.modMatrix; + amplitudeTarget = mm.findTarget(ModKey::createNXYZ(ModId::Amplitude, region->getId())); + volumeTarget = mm.findTarget(ModKey::createNXYZ(ModId::Volume, region->getId())); + panTarget = mm.findTarget(ModKey::createNXYZ(ModId::Pan, region->getId())); + positionTarget = mm.findTarget(ModKey::createNXYZ(ModId::Position, region->getId())); + widthTarget = mm.findTarget(ModKey::createNXYZ(ModId::Width, region->getId())); + pitchTarget = mm.findTarget(ModKey::createNXYZ(ModId::Pitch, region->getId())); +} diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index c8ebb0c8..10e0df2a 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -406,6 +406,12 @@ private: */ void switchState(State s); + /** + * @brief Save the modulation targets to avoid recomputing them in every callback. + * Must be called during startVoice() ideally. + */ + void saveModulationTargets(const Region* region) noexcept; + const NumericId id; StateListener* stateListener = nullptr; @@ -466,6 +472,13 @@ private: Smoother xfadeSmoother; void resetSmoothers() noexcept; + ModMatrix::TargetId amplitudeTarget; + ModMatrix::TargetId volumeTarget; + ModMatrix::TargetId panTarget; + ModMatrix::TargetId positionTarget; + ModMatrix::TargetId widthTarget; + ModMatrix::TargetId pitchTarget; + PowerFollower powerFollower; LEAK_DETECTOR(Voice); From ba53028d8dcbc00b3eafcab26b6bc22bd7b8924d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 9 Sep 2020 17:43:26 +0200 Subject: [PATCH 236/445] Store shortcuts from the region id to the source and targets in the MM --- src/sfizz/modulations/ModMatrix.cpp | 82 ++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index a8c1f8fe..4c9295f0 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -49,6 +49,10 @@ struct ModMatrix::Impl { absl::flat_hash_map sourceIndex_; absl::flat_hash_map targetIndex_; + int maxRegionIdx { -1 }; + std::vector> sourceRegionIndex_; + std::vector> targetRegionIndex_; + std::vector sources_; std::vector targets_; }; @@ -72,6 +76,9 @@ void ModMatrix::clear() impl.targetIndex_.clear(); impl.sources_.clear(); impl.targets_.clear(); + impl.sourceRegionIndex_.clear(); + impl.targetRegionIndex_.clear(); + impl.maxRegionIdx = -1; } void ModMatrix::setSampleRate(double sampleRate) @@ -124,6 +131,8 @@ ModMatrix::SourceId ModMatrix::registerSource(const ModKey& key, ModGenerator& g source.buffer.resize(impl.samplesPerBlock_); impl.sourceIndex_[key] = id.number(); + if (key.region().number() > impl.maxRegionIdx) + impl.maxRegionIdx = key.region().number(); gen.setSampleRate(impl.sampleRate_); gen.setSamplesPerBlock(impl.samplesPerBlock_); @@ -148,6 +157,9 @@ ModMatrix::TargetId ModMatrix::registerTarget(const ModKey& key) target.buffer.resize(impl.samplesPerBlock_); impl.targetIndex_[key] = id.number(); + if (key.region().number() > impl.maxRegionIdx) + impl.maxRegionIdx = key.region().number(); + return id; } @@ -193,20 +205,39 @@ void ModMatrix::init() { Impl& impl = *impl_; - for (Impl::Source &source : impl.sources_) { - const int flags = source.key.flags(); - if (flags & kModIsPerCycle) + if (impl.maxRegionIdx >= 0) { + const size_t numRegions = impl.maxRegionIdx + 1; + impl.sourceRegionIndex_.resize(numRegions); + impl.targetRegionIndex_.resize(numRegions); + } + + for (unsigned i = 0; i < impl.sources_.size(); ++i) { + Impl::Source& source = impl.sources_[i]; + if (source.key.flags() & kModIsPerCycle) source.gen->init(source.key, {}, 0); + + if (source.key.region().number() >= 0) { + impl.sourceRegionIndex_[source.key.region().number()].push_back(i); + } + } + + for (unsigned i = 0; i < impl.targets_.size(); ++i) { + Impl::Target& target = impl.targets_[i]; + if (target.key.region().number() >= 0) + impl.targetRegionIndex_[target.key.region().number()].push_back(i); } } void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, unsigned delay) { Impl& impl = *impl_; + ASSERT(regionId.number() >= 0); + ASSERT(static_cast(regionId.number()) < impl.sourceRegionIndex_.size()); - for (Impl::Source &source : impl.sources_) { - const int flags = source.key.flags(); - if ((flags & kModIsPerVoice) && source.key.region() == regionId) + const auto idNumber = static_cast(regionId.number()); + for (auto idx: impl.sourceRegionIndex_[idNumber]) { + const auto& source = impl.sources_[idx]; + if (source.key.flags() & kModIsPerVoice) source.gen->init(source.key, voiceId, delay); } } @@ -215,9 +246,12 @@ void ModMatrix::releaseVoice(NumericId voiceId, NumericId regionI { Impl& impl = *impl_; - for (Impl::Source &source : impl.sources_) { - const int flags = source.key.flags(); - if ((flags & kModIsPerVoice) && source.key.region() == regionId) + ASSERT(regionId.number() >= 0); + + const auto idNumber = static_cast(regionId.number()); + for (auto idx: impl.sourceRegionIndex_[idNumber]) { + const auto& source = impl.sources_[idx]; + if (source.key.flags() & kModIsPerVoice) source.gen->release(source.key, voiceId, delay); } } @@ -241,8 +275,7 @@ void ModMatrix::endCycle() for (Impl::Source &source : impl.sources_) { if (!source.bufferReady) { - const int flags = source.key.flags(); - if (flags & kModIsPerCycle) { + if (source.key.flags() & kModIsPerCycle) { absl::Span buffer(source.buffer.data(), numFrames); source.gen->generateDiscarded(source.key, {}, buffer); } @@ -259,14 +292,18 @@ void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) impl.currentVoiceId_ = voiceId; impl.currentRegionId_ = regionId; - for (Impl::Source &source : impl.sources_) { - const int flags = source.key.flags(); - if (flags & kModIsPerVoice) + ASSERT(regionId.number() >= 0); + + const auto idNumber = static_cast(regionId.number()); + for (auto idx: impl.sourceRegionIndex_[idNumber]) { + auto& source = impl.sources_[idx]; + if (source.key.flags() & kModIsPerVoice) source.bufferReady = false; } - for (Impl::Target &target : impl.targets_) { - const int flags = target.key.flags(); - if (flags & kModIsPerVoice) + + for (auto idx: impl.targetRegionIndex_[idNumber]) { + auto& target = impl.targets_[idx]; + if (target.key.flags() & kModIsPerVoice) target.bufferReady = false; } } @@ -278,10 +315,15 @@ void ModMatrix::endVoice() const NumericId voiceId = impl.currentVoiceId_; const NumericId regionId = impl.currentRegionId_; - for (Impl::Source &source : impl.sources_) { + ASSERT(regionId.number() >= 0); + ASSERT(static_cast(regionId.number()) < impl.sourceRegionIndex_.size()); + + const auto idNumber = static_cast(regionId.number()); + + for (auto idx: impl.sourceRegionIndex_[idNumber]) { + const auto& source = impl.sources_[idx]; if (!source.bufferReady) { - const int flags = source.key.flags(); - if ((flags & kModIsPerVoice) && source.key.region() == regionId) { + if (source.key.flags() & kModIsPerVoice) { absl::Span buffer(source.buffer.data(), numFrames); source.gen->generateDiscarded(source.key, voiceId, buffer); } From c91c653d8aa4bec99c5c82bf577c07ef03a01232 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 17:47:24 +0200 Subject: [PATCH 237/445] Use the operator bool of NumericId --- src/sfizz/modulations/ModMatrix.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 4c9295f0..a9ca3a38 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -216,14 +216,14 @@ void ModMatrix::init() if (source.key.flags() & kModIsPerCycle) source.gen->init(source.key, {}, 0); - if (source.key.region().number() >= 0) { + if (source.key.region()) { impl.sourceRegionIndex_[source.key.region().number()].push_back(i); } } for (unsigned i = 0; i < impl.targets_.size(); ++i) { Impl::Target& target = impl.targets_[i]; - if (target.key.region().number() >= 0) + if (target.key.region()) impl.targetRegionIndex_[target.key.region().number()].push_back(i); } } @@ -231,7 +231,7 @@ void ModMatrix::init() void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, unsigned delay) { Impl& impl = *impl_; - ASSERT(regionId.number() >= 0); + ASSERT(regionId); ASSERT(static_cast(regionId.number()) < impl.sourceRegionIndex_.size()); const auto idNumber = static_cast(regionId.number()); @@ -246,7 +246,7 @@ void ModMatrix::releaseVoice(NumericId voiceId, NumericId regionI { Impl& impl = *impl_; - ASSERT(regionId.number() >= 0); + ASSERT(regionId); const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceRegionIndex_[idNumber]) { @@ -292,7 +292,7 @@ void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) impl.currentVoiceId_ = voiceId; impl.currentRegionId_ = regionId; - ASSERT(regionId.number() >= 0); + ASSERT(regionId); const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceRegionIndex_[idNumber]) { @@ -315,7 +315,7 @@ void ModMatrix::endVoice() const NumericId voiceId = impl.currentVoiceId_; const NumericId regionId = impl.currentRegionId_; - ASSERT(regionId.number() >= 0); + ASSERT(regionId); ASSERT(static_cast(regionId.number()) < impl.sourceRegionIndex_.size()); const auto idNumber = static_cast(regionId.number()); From 9c5f9537fad69da276179b8dc48696add5156743 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 17:51:05 +0200 Subject: [PATCH 238/445] Adopt the underscore convention for member variables --- src/sfizz/modulations/ModMatrix.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index a9ca3a38..a60397c8 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -49,7 +49,7 @@ struct ModMatrix::Impl { absl::flat_hash_map sourceIndex_; absl::flat_hash_map targetIndex_; - int maxRegionIdx { -1 }; + int maxRegionIdx_ { -1 }; std::vector> sourceRegionIndex_; std::vector> targetRegionIndex_; @@ -78,7 +78,7 @@ void ModMatrix::clear() impl.targets_.clear(); impl.sourceRegionIndex_.clear(); impl.targetRegionIndex_.clear(); - impl.maxRegionIdx = -1; + impl.maxRegionIdx_ = -1; } void ModMatrix::setSampleRate(double sampleRate) @@ -131,8 +131,8 @@ ModMatrix::SourceId ModMatrix::registerSource(const ModKey& key, ModGenerator& g source.buffer.resize(impl.samplesPerBlock_); impl.sourceIndex_[key] = id.number(); - if (key.region().number() > impl.maxRegionIdx) - impl.maxRegionIdx = key.region().number(); + if (key.region().number() > impl.maxRegionIdx_) + impl.maxRegionIdx_ = key.region().number(); gen.setSampleRate(impl.sampleRate_); gen.setSamplesPerBlock(impl.samplesPerBlock_); @@ -157,8 +157,8 @@ ModMatrix::TargetId ModMatrix::registerTarget(const ModKey& key) target.buffer.resize(impl.samplesPerBlock_); impl.targetIndex_[key] = id.number(); - if (key.region().number() > impl.maxRegionIdx) - impl.maxRegionIdx = key.region().number(); + if (key.region().number() > impl.maxRegionIdx_) + impl.maxRegionIdx_ = key.region().number(); return id; } @@ -205,8 +205,8 @@ void ModMatrix::init() { Impl& impl = *impl_; - if (impl.maxRegionIdx >= 0) { - const size_t numRegions = impl.maxRegionIdx + 1; + if (impl.maxRegionIdx_ >= 0) { + const size_t numRegions = impl.maxRegionIdx_ + 1; impl.sourceRegionIndex_.resize(numRegions); impl.targetRegionIndex_.resize(numRegions); } From 0fd5ec57ab494adb931c830efa7421e2303d8b5a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 17:52:24 +0200 Subject: [PATCH 239/445] Rename the variable to make it clearer what it is --- src/sfizz/modulations/ModMatrix.cpp | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index a60397c8..7365c2eb 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -50,8 +50,8 @@ struct ModMatrix::Impl { absl::flat_hash_map targetIndex_; int maxRegionIdx_ { -1 }; - std::vector> sourceRegionIndex_; - std::vector> targetRegionIndex_; + std::vector> sourceIndicesForRegion_; + std::vector> targetIndicesForRegion_; std::vector sources_; std::vector targets_; @@ -76,8 +76,8 @@ void ModMatrix::clear() impl.targetIndex_.clear(); impl.sources_.clear(); impl.targets_.clear(); - impl.sourceRegionIndex_.clear(); - impl.targetRegionIndex_.clear(); + impl.sourceIndicesForRegion_.clear(); + impl.targetIndicesForRegion_.clear(); impl.maxRegionIdx_ = -1; } @@ -207,8 +207,8 @@ void ModMatrix::init() if (impl.maxRegionIdx_ >= 0) { const size_t numRegions = impl.maxRegionIdx_ + 1; - impl.sourceRegionIndex_.resize(numRegions); - impl.targetRegionIndex_.resize(numRegions); + impl.sourceIndicesForRegion_.resize(numRegions); + impl.targetIndicesForRegion_.resize(numRegions); } for (unsigned i = 0; i < impl.sources_.size(); ++i) { @@ -217,14 +217,14 @@ void ModMatrix::init() source.gen->init(source.key, {}, 0); if (source.key.region()) { - impl.sourceRegionIndex_[source.key.region().number()].push_back(i); + impl.sourceIndicesForRegion_[source.key.region().number()].push_back(i); } } for (unsigned i = 0; i < impl.targets_.size(); ++i) { Impl::Target& target = impl.targets_[i]; if (target.key.region()) - impl.targetRegionIndex_[target.key.region().number()].push_back(i); + impl.targetIndicesForRegion_[target.key.region().number()].push_back(i); } } @@ -232,10 +232,10 @@ void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, { Impl& impl = *impl_; ASSERT(regionId); - ASSERT(static_cast(regionId.number()) < impl.sourceRegionIndex_.size()); + ASSERT(static_cast(regionId.number()) < impl.sourceIndicesForRegion_.size()); const auto idNumber = static_cast(regionId.number()); - for (auto idx: impl.sourceRegionIndex_[idNumber]) { + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { const auto& source = impl.sources_[idx]; if (source.key.flags() & kModIsPerVoice) source.gen->init(source.key, voiceId, delay); @@ -249,7 +249,7 @@ void ModMatrix::releaseVoice(NumericId voiceId, NumericId regionI ASSERT(regionId); const auto idNumber = static_cast(regionId.number()); - for (auto idx: impl.sourceRegionIndex_[idNumber]) { + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { const auto& source = impl.sources_[idx]; if (source.key.flags() & kModIsPerVoice) source.gen->release(source.key, voiceId, delay); @@ -295,13 +295,13 @@ void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) ASSERT(regionId); const auto idNumber = static_cast(regionId.number()); - for (auto idx: impl.sourceRegionIndex_[idNumber]) { + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { auto& source = impl.sources_[idx]; if (source.key.flags() & kModIsPerVoice) source.bufferReady = false; } - for (auto idx: impl.targetRegionIndex_[idNumber]) { + for (auto idx: impl.targetIndicesForRegion_[idNumber]) { auto& target = impl.targets_[idx]; if (target.key.flags() & kModIsPerVoice) target.bufferReady = false; @@ -316,11 +316,11 @@ void ModMatrix::endVoice() const NumericId regionId = impl.currentRegionId_; ASSERT(regionId); - ASSERT(static_cast(regionId.number()) < impl.sourceRegionIndex_.size()); + ASSERT(static_cast(regionId.number()) < impl.sourceIndicesForRegion_.size()); const auto idNumber = static_cast(regionId.number()); - for (auto idx: impl.sourceRegionIndex_[idNumber]) { + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { const auto& source = impl.sources_[idx]; if (!source.bufferReady) { if (source.key.flags() & kModIsPerVoice) { From aaa51f6dfe3e5c6bbb3d90427e91e898204f2bab Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 17:56:42 +0200 Subject: [PATCH 240/445] Use the flags, and assert key to be valid with said flags --- src/sfizz/modulations/ModMatrix.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 7365c2eb..68501b40 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -213,18 +213,24 @@ void ModMatrix::init() for (unsigned i = 0; i < impl.sources_.size(); ++i) { Impl::Source& source = impl.sources_[i]; - if (source.key.flags() & kModIsPerCycle) + const int flags = source.key.flags(); + if (flags & kModIsPerCycle) { + ASSERT(!source.key.region()); source.gen->init(source.key, {}, 0); - - if (source.key.region()) { + } + else if (flags & kModIsPerVoice) { + ASSERT(source.key.region()); impl.sourceIndicesForRegion_[source.key.region().number()].push_back(i); } } for (unsigned i = 0; i < impl.targets_.size(); ++i) { Impl::Target& target = impl.targets_[i]; - if (target.key.region()) + const int flags = target.key.flags(); + if (flags & kModIsPerVoice) { + ASSERT(target.key.region()); impl.targetIndicesForRegion_[target.key.region().number()].push_back(i); + } } } From 89893503bef6068854efac95db7a827e4e08a6df Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 17:58:42 +0200 Subject: [PATCH 241/445] Remove the conditions which are made redundant --- src/sfizz/modulations/ModMatrix.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 68501b40..06a5fe2b 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -243,8 +243,7 @@ void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { const auto& source = impl.sources_[idx]; - if (source.key.flags() & kModIsPerVoice) - source.gen->init(source.key, voiceId, delay); + source.gen->init(source.key, voiceId, delay); } } @@ -257,8 +256,7 @@ void ModMatrix::releaseVoice(NumericId voiceId, NumericId regionI const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { const auto& source = impl.sources_[idx]; - if (source.key.flags() & kModIsPerVoice) - source.gen->release(source.key, voiceId, delay); + source.gen->release(source.key, voiceId, delay); } } @@ -303,14 +301,12 @@ void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { auto& source = impl.sources_[idx]; - if (source.key.flags() & kModIsPerVoice) - source.bufferReady = false; + source.bufferReady = false; } for (auto idx: impl.targetIndicesForRegion_[idNumber]) { auto& target = impl.targets_[idx]; - if (target.key.flags() & kModIsPerVoice) - target.bufferReady = false; + target.bufferReady = false; } } @@ -329,10 +325,8 @@ void ModMatrix::endVoice() for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { const auto& source = impl.sources_[idx]; if (!source.bufferReady) { - if (source.key.flags() & kModIsPerVoice) { - absl::Span buffer(source.buffer.data(), numFrames); - source.gen->generateDiscarded(source.key, voiceId, buffer); - } + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, voiceId, buffer); } } From 8d08c6700d5644ca0fa9141b8ffc9fdde5852451 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 11 Sep 2020 18:07:01 +0200 Subject: [PATCH 242/445] Replace auto for ease of reading --- src/sfizz/modulations/ModMatrix.cpp | 39 +++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 06a5fe2b..4533d156 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -49,6 +49,9 @@ struct ModMatrix::Impl { absl::flat_hash_map sourceIndex_; absl::flat_hash_map targetIndex_; + std::vector sourceIndicesForGlobal_; + std::vector targetIndicesForGlobal_; + int maxRegionIdx_ { -1 }; std::vector> sourceIndicesForRegion_; std::vector> targetIndicesForRegion_; @@ -76,6 +79,8 @@ void ModMatrix::clear() impl.targetIndex_.clear(); impl.sources_.clear(); impl.targets_.clear(); + impl.sourceIndicesForGlobal_.clear(); + impl.targetIndicesForGlobal_.clear(); impl.sourceIndicesForRegion_.clear(); impl.targetIndicesForRegion_.clear(); impl.maxRegionIdx_ = -1; @@ -217,6 +222,7 @@ void ModMatrix::init() if (flags & kModIsPerCycle) { ASSERT(!source.key.region()); source.gen->init(source.key, {}, 0); + impl.sourceIndicesForGlobal_.push_back(i); } else if (flags & kModIsPerVoice) { ASSERT(source.key.region()); @@ -227,7 +233,11 @@ void ModMatrix::init() for (unsigned i = 0; i < impl.targets_.size(); ++i) { Impl::Target& target = impl.targets_[i]; const int flags = target.key.flags(); - if (flags & kModIsPerVoice) { + if (flags & kModIsPerCycle) { + ASSERT(!target.key.region()); + impl.targetIndicesForGlobal_.push_back(i); + } + else if (flags & kModIsPerVoice) { ASSERT(target.key.region()); impl.targetIndicesForRegion_[target.key.region().number()].push_back(i); } @@ -242,7 +252,7 @@ void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { - const auto& source = impl.sources_[idx]; + const Impl::Source& source = impl.sources_[idx]; source.gen->init(source.key, voiceId, delay); } } @@ -255,7 +265,7 @@ void ModMatrix::releaseVoice(NumericId voiceId, NumericId regionI const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { - const auto& source = impl.sources_[idx]; + const Impl::Source& source = impl.sources_[idx]; source.gen->release(source.key, voiceId, delay); } } @@ -266,10 +276,14 @@ void ModMatrix::beginCycle(unsigned numFrames) impl.numFrames_ = numFrames; - for (Impl::Source &source : impl.sources_) + for (auto idx: impl.sourceIndicesForGlobal_) { + Impl::Source& source = impl.sources_[idx]; source.bufferReady = false; - for (Impl::Target &target : impl.targets_) + } + for (auto idx: impl.targetIndicesForGlobal_) { + Impl::Target& target = impl.targets_[idx]; target.bufferReady = false; + } } void ModMatrix::endCycle() @@ -277,12 +291,11 @@ void ModMatrix::endCycle() Impl& impl = *impl_; const uint32_t numFrames = impl.numFrames_; - for (Impl::Source &source : impl.sources_) { + for (auto idx: impl.sourceIndicesForGlobal_) { + Impl::Source& source = impl.sources_[idx]; if (!source.bufferReady) { - if (source.key.flags() & kModIsPerCycle) { - absl::Span buffer(source.buffer.data(), numFrames); - source.gen->generateDiscarded(source.key, {}, buffer); - } + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, {}, buffer); } } @@ -300,12 +313,12 @@ void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { - auto& source = impl.sources_[idx]; + Impl::Source& source = impl.sources_[idx]; source.bufferReady = false; } for (auto idx: impl.targetIndicesForRegion_[idNumber]) { - auto& target = impl.targets_[idx]; + Impl::Target& target = impl.targets_[idx]; target.bufferReady = false; } } @@ -323,7 +336,7 @@ void ModMatrix::endVoice() const auto idNumber = static_cast(regionId.number()); for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { - const auto& source = impl.sources_[idx]; + const Impl::Source& source = impl.sources_[idx]; if (!source.bufferReady) { absl::Span buffer(source.buffer.data(), numFrames); source.gen->generateDiscarded(source.key, voiceId, buffer); From b692377f7b0cbdd5adfcd76ca72c452f5ffa3340 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 20 Aug 2020 23:37:23 +0200 Subject: [PATCH 243/445] Add the Flex EG --- dpf.mk | 3 + src/CMakeLists.txt | 6 + src/sfizz/Curve.cpp | 7 + src/sfizz/Curve.h | 10 +- src/sfizz/Defaults.h | 14 ++ src/sfizz/FlexEGDescription.cpp | 87 ++++++++ src/sfizz/FlexEGDescription.h | 39 ++++ src/sfizz/FlexEnvelope.cpp | 206 ++++++++++++++++++ src/sfizz/FlexEnvelope.h | 53 +++++ src/sfizz/Region.cpp | 141 ++++++++++++ src/sfizz/Region.h | 4 + src/sfizz/Synth.cpp | 11 + src/sfizz/Synth.h | 3 + src/sfizz/Voice.cpp | 12 + src/sfizz/Voice.h | 14 ++ .../modulations/sources/FlexEnvelope.cpp | 86 ++++++++ src/sfizz/modulations/sources/FlexEnvelope.h | 24 ++ 17 files changed, 717 insertions(+), 3 deletions(-) create mode 100644 src/sfizz/FlexEGDescription.cpp create mode 100644 src/sfizz/FlexEGDescription.h create mode 100644 src/sfizz/FlexEnvelope.cpp create mode 100644 src/sfizz/FlexEnvelope.h create mode 100644 src/sfizz/modulations/sources/FlexEnvelope.cpp create mode 100644 src/sfizz/modulations/sources/FlexEnvelope.h diff --git a/dpf.mk b/dpf.mk index 2ee507e8..1c8159fa 100644 --- a/dpf.mk +++ b/dpf.mk @@ -66,6 +66,8 @@ SFIZZ_SOURCES = \ src/sfizz/modulations/ModKeyHash.cpp \ src/sfizz/modulations/ModMatrix.cpp \ src/sfizz/modulations/sources/Controller.cpp \ + src/sfizz/modulations/sources/FlexEGDescription.cpp \ + src/sfizz/modulations/sources/FlexEnvelope.cpp \ src/sfizz/modulations/sources/LFO.cpp \ src/sfizz/effects/Compressor.cpp \ src/sfizz/effects/Disto.cpp \ @@ -91,6 +93,7 @@ SFIZZ_SOURCES = \ src/sfizz/FileMetadata.cpp \ src/sfizz/FilePool.cpp \ src/sfizz/FilterPool.cpp \ + src/sfizz/FlexEnvelope.cpp \ src/sfizz/FloatEnvelopes.cpp \ src/sfizz/Logger.cpp \ src/sfizz/LFO.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6083c110..4adbe3fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ set (SFIZZ_HEADERS sfizz/modulations/ModMatrix.h sfizz/modulations/ModGenerator.h sfizz/modulations/sources/Controller.h + sfizz/modulations/sources/FlexEnvelope.h sfizz/modulations/sources/LFO.h sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h @@ -67,6 +68,8 @@ set (SFIZZ_HEADERS sfizz/FilePool.h sfizz/FilterDescription.h sfizz/FilterPool.h + sfizz/FlexEGDescription.h + sfizz/FlexEnvelope.h sfizz/HistoricalBuffer.h sfizz/Interpolators.h sfizz/Interpolators.hpp @@ -139,11 +142,14 @@ set (SFIZZ_SOURCES sfizz/LFO.cpp sfizz/LFODescription.cpp sfizz/PowerFollower.cpp + sfizz/FlexEGDescription.cpp + sfizz/FlexEnvelope.cpp sfizz/modulations/ModId.cpp sfizz/modulations/ModKey.cpp sfizz/modulations/ModKeyHash.cpp sfizz/modulations/ModMatrix.cpp sfizz/modulations/sources/Controller.cpp + sfizz/modulations/sources/FlexEnvelope.cpp sfizz/modulations/sources/LFO.cpp sfizz/utility/SpinMutex.cpp sfizz/effects/Nothing.cpp diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 89320db5..8d204a39 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -147,6 +147,13 @@ Curve Curve::buildBipolar(float v1, float v2) return curve; } +Curve Curve::buildFromPoints(const float points[NumValues]) +{ + Curve curve; + copy(absl::MakeConstSpan(points, NumValues), absl::Span(curve._points)); + return curve; +} + const Curve& Curve::getDefault() { return defaultCurve; diff --git a/src/sfizz/Curve.h b/src/sfizz/Curve.h index 3cf4b2cb..5c0ed361 100644 --- a/src/sfizz/Curve.h +++ b/src/sfizz/Curve.h @@ -21,6 +21,8 @@ struct Opcode; */ class Curve { public: + enum { NumValues = 128 }; + /** * @brief Compute the curve for integral x in domain [0:127] */ @@ -93,14 +95,16 @@ public: */ static Curve buildBipolar(float v1, float v2); + /** + * @brief Build a curve from a table of points + */ + static Curve buildFromPoints(const float points[NumValues]); + /** * @brief Get a linear curve from 0 to 1 */ static const Curve& getDefault(); -private: - enum { NumValues = 128 }; - private: void fill(Interpolator itp, const bool fillStatus[NumValues]); void lerpFill(const bool fillStatus[NumValues]); diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index d53f3772..b12d1b8a 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -244,6 +244,20 @@ namespace Default constexpr Range egOnCCTimeRange { -100.0, 100.0 }; constexpr Range egOnCCPercentRange { -100.0, 100.0 }; + // Flex envelope generators + constexpr int numFlexEGs { 4 }; + constexpr int numFlexEGPoints { 8 }; + constexpr int flexEGDynamic { 0 }; + constexpr int flexEGSustain { 0 }; + constexpr float flexEGPointTime { 0 }; + constexpr float flexEGPointLevel { 0 }; + constexpr float flexEGPointShape { 0 }; + constexpr Range flexEGDynamicRange { 0, 1 }; + constexpr Range flexEGSustainRange { 0, 100 }; + constexpr Range flexEGPointTimeRange { 0.0f, 100.0f }; + constexpr Range flexEGPointLevelRange { -1.0f, 1.0f }; + constexpr Range flexEGPointShapeRange { -100.0f, 100.0f }; + // ***** SFZ v2 ******** constexpr int sampleQuality { 1 }; constexpr int sampleQualityInFreewheelingMode { 10 }; // for future use, possibly excessive diff --git a/src/sfizz/FlexEGDescription.cpp b/src/sfizz/FlexEGDescription.cpp new file mode 100644 index 00000000..18ea92e3 --- /dev/null +++ b/src/sfizz/FlexEGDescription.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "FlexEGDescription.h" +#include "Curve.h" +#include +#include + +namespace sfz { + +void FlexEGPoint::setShape(float shape) +{ + shape_ = shape; + shapeCurve_ = FlexEGs::getShapeCurve(shape); +} + +const Curve& FlexEGPoint::curve() const +{ + if (shapeCurve_) + return *shapeCurve_; + else + return Curve::getDefault(); +} + +/// +typedef absl::flat_hash_map> FlexEGShapes; + +static FlexEGShapes& getShapeMap() +{ + static FlexEGShapes shapes; + return shapes; +} + +std::shared_ptr FlexEGs::getShapeCurve(float shape) +{ + static FlexEGShapes& map = getShapeMap(); + + std::weak_ptr& slot = map[shape]; + + std::shared_ptr curve = slot.lock(); + if (curve) + return curve; + + curve.reset(new Curve); + + /// + constexpr unsigned numPoints = Curve::NumValues; + float points[numPoints]; + + if (shape == 0) + *curve = Curve::getDefault(); + else if (shape > 0) { + for (unsigned i = 0; i < numPoints; ++i) { + float x = float(i) / (numPoints - 1); + points[i] = std::pow(x, shape); + } + *curve = Curve::buildFromPoints(points); + } + else if (shape < 0) { + for (unsigned i = 0; i < numPoints; ++i) { + float x = float(i) / (numPoints - 1); + points[i] = 1 - std::pow(1 - x, -shape); + } + *curve = Curve::buildFromPoints(points); + } + + /// + slot = curve; + return curve; +} + +void FlexEGs::clearUnusedCurves() +{ + static FlexEGShapes& map = getShapeMap(); + + for (auto it = map.begin(); it != map.end(); ) { + if (it->second.use_count() == 0) + map.erase(it++); + else + ++it; + } +} + +} // namespace sfz diff --git a/src/sfizz/FlexEGDescription.h b/src/sfizz/FlexEGDescription.h new file mode 100644 index 00000000..fd8e9428 --- /dev/null +++ b/src/sfizz/FlexEGDescription.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Defaults.h" +#include +#include + +namespace sfz { +class Curve; + +namespace FlexEGs { + std::shared_ptr getShapeCurve(float shape); + void clearUnusedCurves(); +}; + +struct FlexEGPoint { + float time { Default::flexEGPointTime }; // duration until next step (s) + float level { Default::flexEGPointLevel }; // normalized amplitude + + void setShape(float shape); + float shape() const noexcept { return shape_; } + const Curve& curve() const; + +private: + float shape_ { Default::flexEGPointShape }; // 0: linear, positive: exp, negative: log + std::shared_ptr shapeCurve_; +}; + +struct FlexEGDescription { + int dynamic { Default::flexEGDynamic }; // whether parameters can be modulated while EG runs + int sustain { Default::flexEGSustain }; // index of the sustain point (default to 0 in ARIA) + std::vector points; +}; + +} // namespace sfz diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp new file mode 100644 index 00000000..3810ec73 --- /dev/null +++ b/src/sfizz/FlexEnvelope.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +/* + Note(jpc): implementation status + +- [ ] egN_points (purpose unknown) +- [x] egN_timeX +- [x] egN_levelX +- [x] egN_shapeX +- [x] egN_sustain +- [ ] egN_dynamic +*/ + +#include "FlexEnvelope.h" +#include "FlexEGDescription.h" +#include "Curve.h" +#include "Config.h" +#include "SIMDHelpers.h" +#include + +namespace sfz { + +struct FlexEnvelope::Impl { + const FlexEGDescription* desc_ { nullptr }; + float samplePeriod_ { 1.0 / config::defaultSampleRate }; + size_t delayFramesLeft_ { 0 }; + + // + float stageSourceLevel_ { 0.0 }; + float stageTargetLevel_ { 0.0 }; + float stageTime_ { 0.0 }; + bool stageSustained_ { false }; + const Curve* stageCurve_ { nullptr }; + + // + unsigned currentStageNumber_ { 0 }; + float currentLevel_ { 0.0 }; + float currentTime_ { 0.0 }; + absl::optional currentFramesUntilRelease_ { absl::nullopt }; + bool isReleased_ { false }; + + // + void process(absl::Span out); + bool advanceToNextStage(); +}; + +FlexEnvelope::FlexEnvelope() + : impl_(new Impl) +{ +} + +FlexEnvelope::~FlexEnvelope() +{ +} + +void FlexEnvelope::setSampleRate(double sampleRate) +{ + Impl& impl = *impl_; + impl.samplePeriod_ = 1.0 / sampleRate; +} + +void FlexEnvelope::configure(const FlexEGDescription* desc) +{ + Impl& impl = *impl_; + impl.desc_ = desc; +} + +void FlexEnvelope::start(unsigned triggerDelay) +{ + Impl& impl = *impl_; + const FlexEGDescription& desc = *impl.desc_; + + impl.delayFramesLeft_ = triggerDelay; + + FlexEGPoint point; + if (!desc.points.empty()) + point = desc.points[0]; + + // + impl.stageSourceLevel_ = 0.0; + impl.stageTargetLevel_ = point.level; + impl.stageTime_ = point.time; + impl.stageSustained_ = desc.sustain == 0; + impl.stageCurve_ = &point.curve(); + impl.currentFramesUntilRelease_ = absl::nullopt; + impl.isReleased_ = false; + + // + impl.currentStageNumber_ = 0; + impl.currentLevel_ = 0.0; + impl.currentTime_ = 0.0; +} + +void FlexEnvelope::release(unsigned releaseDelay) +{ + Impl& impl = *impl_; + impl.currentFramesUntilRelease_ = releaseDelay; +} + +void FlexEnvelope::process(absl::Span out) +{ + Impl& impl = *impl_; + impl.process(out); +} + +void FlexEnvelope::Impl::process(absl::Span out) +{ + const FlexEGDescription& desc = *desc_; + size_t numFrames = out.size(); + const float samplePeriod = samplePeriod_; + + // Skip the initial delay, for frame-accurate trigger + size_t skipFrames = std::min(numFrames, delayFramesLeft_); + if (skipFrames > 0) { + delayFramesLeft_ -= skipFrames; + fill(absl::MakeSpan(out.data(), skipFrames), 0.0f); + out.remove_prefix(skipFrames); + numFrames -= skipFrames; + } + + // Envelope finished? + if (currentStageNumber_ >= desc.points.size()) { + fill(out, 0.0f); + return; + } + + size_t frameIndex = 0; + absl::optional framesUntilRelease = currentFramesUntilRelease_; + + while (frameIndex < numFrames) { + // Check for release + if (framesUntilRelease && *framesUntilRelease == 0) { + isReleased_ = true; + framesUntilRelease = absl::nullopt; + } + + // Perform stage transitions + const bool isReleased = isReleased_; + while ((!stageSustained_ && currentTime_ >= stageTime_) || + (stageSustained_ && isReleased)) { + if (!advanceToNextStage()) { + fill(out, 0.0f); + return; + } + } + + // Process without going past the release point, if there is one + size_t maxFrameIndex = numFrames; + if (framesUntilRelease) + maxFrameIndex = std::min(maxFrameIndex, frameIndex + *framesUntilRelease); + + // Process the current stage + float time = currentTime_; + float level = currentLevel_; + const float stageEndTime = stageTime_; + const float sourceLevel = stageSourceLevel_; + const float targetLevel = stageTargetLevel_; + const bool sustained = stageSustained_; + const Curve& curve = *stageCurve_; + size_t framesDone = 0; + while ((time < stageEndTime || sustained) && frameIndex < maxFrameIndex) { + float x = time * (1.0f / stageEndTime); + float c = curve.evalNormalized(x); + level = sourceLevel + c * (targetLevel - sourceLevel); + out[frameIndex++] = level; + time += samplePeriod; + ++framesDone; + } + currentLevel_ = level; + + // Update the counter to release + if (framesUntilRelease) + *framesUntilRelease -= framesDone; + + currentTime_ = time; + } + + currentFramesUntilRelease_ = framesUntilRelease; +} + +bool FlexEnvelope::Impl::advanceToNextStage() +{ + const FlexEGDescription& desc = *desc_; + + unsigned nextStageNo = currentStageNumber_ + 1; + currentStageNumber_ = nextStageNo; + + if (currentStageNumber_ >= desc.points.size()) + return false; + + const FlexEGPoint& point = desc.points[currentStageNumber_]; + stageSourceLevel_ = currentLevel_; + stageTargetLevel_ = point.level; + stageTime_ = point.time; + stageSustained_ = int(nextStageNo) == desc.sustain; + stageCurve_ = &point.curve(); + + currentTime_ = 0; + return true; +}; + +} // namespace sfz diff --git a/src/sfizz/FlexEnvelope.h b/src/sfizz/FlexEnvelope.h new file mode 100644 index 00000000..3316e715 --- /dev/null +++ b/src/sfizz/FlexEnvelope.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +namespace sfz { +struct FlexEGDescription; + +/** + Flex envelope generator (according to ARIA) + */ +class FlexEnvelope { +public: + FlexEnvelope(); + ~FlexEnvelope(); + + /** + Sets the sample rate. + */ + void setSampleRate(double sampleRate); + + /** + Attach some control parameters to this EG. + The control structure is owned by the caller. + */ + void configure(const FlexEGDescription* desc); + + /** + Start processing an EG as a region is triggered. + */ + void start(unsigned triggerDelay); + + /** + Release the EG. + */ + void release(unsigned releaseDelay); + + /** + Process a cycle of the generator. + */ + void process(absl::Span out); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index b64af303..a6cea577 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1046,6 +1046,80 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) } break; + // Modulation: Flex EG (targets) + case hash("eg&_amplitude"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Amplitude, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_pan"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::panCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Pan, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_width"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::widthCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Width, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_position"): // sfizz extension + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::positionCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Position, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_pitch"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::tuneCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Pitch, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + case hash("eg&_volume"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (auto value = readOpcode(opcode.value, Default::volumeCCRange)) { + const ModKey source = ModKey::createNXYZ(ModId::Envelope, id, egNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Volume, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; + // Amplitude Envelope case hash("ampeg_attack"): setValueFromOpcode(opcode, amplitudeEG.attack, Default::egTimeRange); @@ -1155,6 +1229,73 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) break; + // Flex envelopes + case hash("eg&_dynamic"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + setValueFromOpcode(opcode, eg.dynamic, Default::flexEGDynamicRange); + } + break; + case hash("eg&_sustain"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + setValueFromOpcode(opcode, eg.sustain, Default::flexEGSustainRange); + } + break; + case hash("eg&_time&"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + const auto pointNumber = opcode.parameters[1]; + if (!extendIfNecessary(eg.points, pointNumber + 1, Default::numFlexEGPoints)) + return false; + setValueFromOpcode(opcode, eg.points[pointNumber].time, Default::flexEGPointTimeRange); + } + break; + case hash("eg&_level&"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + const auto pointNumber = opcode.parameters[1]; + if (!extendIfNecessary(eg.points, pointNumber + 1, Default::numFlexEGPoints)) + return false; + setValueFromOpcode(opcode, eg.points[pointNumber].level, Default::flexEGPointLevelRange); + } + break; + case hash("eg&_shape&"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + auto& eg = flexEGs[egNumber - 1]; + const auto pointNumber = opcode.parameters[1]; + if (!extendIfNecessary(eg.points, pointNumber + 1, Default::numFlexEGPoints)) + return false; + if (auto value = readOpcode(opcode.value, Default::flexEGPointShapeRange)) + eg.points[pointNumber].setShape(*value); + } + break; + case hash("effect&"): { const auto effectNumber = opcode.parameters.back(); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 4635f78c..4c9dabba 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -10,6 +10,7 @@ #include "LeakDetector.h" #include "Defaults.h" #include "EGDescription.h" +#include "FlexEGDescription.h" #include "EQDescription.h" #include "FilterDescription.h" #include "LFODescription.h" @@ -379,6 +380,9 @@ struct Region { EGDescription pitchEG; EGDescription filterEG; + // Envelopes + std::vector flexEGs; + // LFOs std::vector lfos; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index beae4e64..e69cd215 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -18,6 +18,7 @@ #include "modulations/ModId.h" #include "modulations/sources/Controller.h" #include "modulations/sources/LFO.h" +#include "modulations/sources/FlexEnvelope.h" #include "pugixml.hpp" #include "absl/algorithm/container.h" #include "absl/memory/memory.h" @@ -46,6 +47,7 @@ sfz::Synth::Synth(int numVoices) // modulation sources genController.reset(new ControllerSource(resources)); genLFO.reset(new LFOSource(*this)); + genFlexEnvelope.reset(new FlexEnvelopeSource(*this)); } sfz::Synth::~Synth() @@ -480,6 +482,9 @@ void sfz::Synth::finalizeSfzLoad() size_t maxFilters { 0 }; size_t maxEQs { 0 }; size_t maxLFOs { 0 }; + size_t maxFlexEGs { 0 }; + + FlexEGs::clearUnusedCurves(); while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); @@ -595,6 +600,7 @@ void sfz::Synth::finalizeSfzLoad() maxFilters = max(maxFilters, region->filters.size()); maxEQs = max(maxEQs, region->equalizers.size()); maxLFOs = max(maxLFOs, region->lfos.size()); + maxFlexEGs = max(maxFlexEGs, region->flexEGs.size()); ++currentRegionIndex; } @@ -605,6 +611,7 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; settingsPerVoice.maxLFOs = maxLFOs; + settingsPerVoice.maxFlexEGs = maxFlexEGs; applySettingsPerVoice(); @@ -1473,6 +1480,7 @@ void sfz::Synth::applySettingsPerVoice() voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); voice->setMaxLFOsPerVoice(settingsPerVoice.maxLFOs); + voice->setMaxFlexEGsPerVoice(settingsPerVoice.maxFlexEGs); } } @@ -1491,6 +1499,9 @@ void sfz::Synth::setupModMatrix() case ModId::LFO: gen = genLFO.get(); break; + case ModId::Envelope: + gen = genFlexEnvelope.get(); + break; default: DBG("[sfizz] Have unknown type of source generator"); break; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 995bb5e6..fd019dc3 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -28,6 +28,7 @@ namespace sfz { class ControllerSource; class LFOSource; +class FlexEnvelopeSource; /** * @brief This class is the core of the sfizz library. In C++ it is the main point @@ -897,12 +898,14 @@ private: // Modulation source generators std::unique_ptr genController; std::unique_ptr genLFO; + std::unique_ptr genFlexEnvelope; // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; size_t maxEQs { 0 }; size_t maxLFOs { 0 }; + size_t maxFlexEGs { 0 }; }; SettingsPerVoice settingsPerVoice; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index b19c6cb5..cb604633 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -13,6 +13,7 @@ #include "Panning.h" #include "SfzHelpers.h" #include "LFO.h" +#include "FlexEnvelope.h" #include "modulations/ModId.h" #include "modulations/ModKey.h" #include "modulations/ModMatrix.h" @@ -798,6 +799,17 @@ void sfz::Voice::setMaxLFOsPerVoice(size_t numLFOs) } } +void sfz::Voice::setMaxFlexEGsPerVoice(size_t numFlexEGs) +{ + flexEGs.resize(numFlexEGs); + + for (size_t i = 0; i < numFlexEGs; ++i) { + auto eg = absl::make_unique(); + eg->setSampleRate(sampleRate); + flexEGs[i] = std::move(eg); + } +} + void sfz::Voice::setupOscillatorUnison() { int m = region->oscillatorMulti; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 10e0df2a..8833271a 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -25,6 +25,7 @@ namespace sfz { enum InterpolatorModel : int; class LFO; +class FlexEnvelope; /** * @brief The SFZ voice are the polyphony holders. They get activated by the synth * and tasked to play a given region until the end, stopping on note-offs, off-groups @@ -263,6 +264,12 @@ public: * @param index */ LFO* getLFO(size_t index) { return lfos[index].get(); } + /** + * @brief Get the Flex EG designated by the given index + * + * @param index + */ + FlexEnvelope* getFlexEG(size_t index) { return flexEGs[index].get(); } /** * @brief Set the max number of filters per voice * @@ -281,6 +288,12 @@ public: * @param numLFOs */ void setMaxLFOsPerVoice(size_t numLFOs); + /** + * @brief Set the max number of Flex EGs per voice + * + * @param numFlexEGs + */ + void setMaxFlexEGsPerVoice(size_t numFlexEGs); /** * @brief Release the voice after a given delay * @@ -444,6 +457,7 @@ private: std::vector filters; std::vector equalizers; std::vector> lfos; + std::vector> flexEGs; ADSREnvelope egEnvelope; float bendStepFactor { centsFactor(1) }; diff --git a/src/sfizz/modulations/sources/FlexEnvelope.cpp b/src/sfizz/modulations/sources/FlexEnvelope.cpp new file mode 100644 index 00000000..557dd4db --- /dev/null +++ b/src/sfizz/modulations/sources/FlexEnvelope.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "FlexEnvelope.h" +#include "../../FlexEnvelope.h" +#include "../../Synth.h" +#include "../../Voice.h" +#include "../../SIMDHelpers.h" +#include "../../Config.h" +#include "../../Debug.h" + +namespace sfz { + +FlexEnvelopeSource::FlexEnvelopeSource(Synth &synth) + : synth_(&synth) +{ +} + +void FlexEnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + Synth& synth = *synth_; + unsigned egIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (egIndex >= region->flexEGs.size()) { + ASSERTFALSE; + return; + } + + FlexEnvelope* eg = voice->getFlexEG(egIndex); + eg->configure(®ion->flexEGs[egIndex]); + eg->start(delay); +} + +void FlexEnvelopeSource::release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + Synth& synth = *synth_; + unsigned egIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (egIndex >= region->flexEGs.size()) { + ASSERTFALSE; + return; + } + + FlexEnvelope* eg = voice->getFlexEG(egIndex); + eg->release(delay); +} + +void FlexEnvelopeSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + Synth& synth = *synth_; + unsigned egIndex = sourceKey.parameters().N; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + if (egIndex >= region->flexEGs.size()) { + ASSERTFALSE; + return; + } + + FlexEnvelope* eg = voice->getFlexEG(egIndex); + eg->process(buffer); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/FlexEnvelope.h b/src/sfizz/modulations/sources/FlexEnvelope.h new file mode 100644 index 00000000..c4e50fc4 --- /dev/null +++ b/src/sfizz/modulations/sources/FlexEnvelope.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../ModGenerator.h" + +namespace sfz { +class Synth; + +class FlexEnvelopeSource : public ModGenerator { +public: + explicit FlexEnvelopeSource(Synth &synth); + void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + Synth* synth_ = nullptr; +}; + +} // namespace sfz From 84bb5532926c63415b373cdcf6566729d100b9ea Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 22 Aug 2020 01:03:10 +0200 Subject: [PATCH 244/445] Some changes of little impact --- src/sfizz/FlexEnvelope.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index 3810ec73..c3e09359 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -189,10 +189,10 @@ bool FlexEnvelope::Impl::advanceToNextStage() unsigned nextStageNo = currentStageNumber_ + 1; currentStageNumber_ = nextStageNo; - if (currentStageNumber_ >= desc.points.size()) + if (nextStageNo >= desc.points.size()) return false; - const FlexEGPoint& point = desc.points[currentStageNumber_]; + const FlexEGPoint& point = desc.points[nextStageNo]; stageSourceLevel_ = currentLevel_; stageTargetLevel_ = point.level; stageTime_ = point.time; From d2f77f63dc1199280c3a4ae1dff853eff7a40f62 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 22 Aug 2020 01:11:17 +0200 Subject: [PATCH 245/445] Ensure to update all variables on early return --- src/sfizz/FlexEnvelope.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index c3e09359..abc2b4a0 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -129,13 +129,12 @@ void FlexEnvelope::Impl::process(absl::Span out) } size_t frameIndex = 0; - absl::optional framesUntilRelease = currentFramesUntilRelease_; while (frameIndex < numFrames) { // Check for release - if (framesUntilRelease && *framesUntilRelease == 0) { + if (currentFramesUntilRelease_ && *currentFramesUntilRelease_ == 0) { isReleased_ = true; - framesUntilRelease = absl::nullopt; + currentFramesUntilRelease_ = absl::nullopt; } // Perform stage transitions @@ -150,8 +149,8 @@ void FlexEnvelope::Impl::process(absl::Span out) // Process without going past the release point, if there is one size_t maxFrameIndex = numFrames; - if (framesUntilRelease) - maxFrameIndex = std::min(maxFrameIndex, frameIndex + *framesUntilRelease); + if (currentFramesUntilRelease_) + maxFrameIndex = std::min(maxFrameIndex, frameIndex + *currentFramesUntilRelease_); // Process the current stage float time = currentTime_; @@ -173,13 +172,11 @@ void FlexEnvelope::Impl::process(absl::Span out) currentLevel_ = level; // Update the counter to release - if (framesUntilRelease) - *framesUntilRelease -= framesDone; + if (currentFramesUntilRelease_) + *currentFramesUntilRelease_ -= framesDone; currentTime_ = time; } - - currentFramesUntilRelease_ = framesUntilRelease; } bool FlexEnvelope::Impl::advanceToNextStage() From 62ee1abd241faa0555d35e6bd2bff69892b02e0b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 12 Sep 2020 02:03:07 +0200 Subject: [PATCH 246/445] Comment on unimplemented loop opcodes --- src/sfizz/FlexEnvelope.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index abc2b4a0..321093fc 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -13,6 +13,9 @@ - [x] egN_shapeX - [x] egN_sustain - [ ] egN_dynamic +- [ ] egN_loop +- [ ] egN_loop_shape +- [ ] egN_loop_count */ #include "FlexEnvelope.h" From 5ee5fa85f85deaccf08d3b1bb4e91dc1246c0e0d Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 13 Sep 2020 16:54:09 +0200 Subject: [PATCH 247/445] Implement the styled knobs --- editor/layout/main.fl | 10 ++-- editor/src/editor/Editor.cpp | 17 ++++++ editor/src/editor/GUIComponents.cpp | 92 +++++++++++++++++++++++++++++ editor/src/editor/GUIComponents.h | 22 +++++++ editor/src/editor/layout/main.hpp | 4 +- 5 files changed, 138 insertions(+), 7 deletions(-) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index 30e3488a..dffc20d7 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -3,7 +3,7 @@ version 1.0305 header_name {.h} code_name {.cxx} widget_class mainView {open - xywh {572 266 800 475} type Double + xywh {576 416 800 475} type Double class LogicalGroup visible } { Fl_Box {} { @@ -128,7 +128,7 @@ widget_class mainView {open Fl_Dial volumeSlider_ { comment {tag=kTagSetVolume} xywh {680 20 48 48} value 0.5 - class Knob48 + class StyledKnob } Fl_Box volumeLabel_ { label {0.0 dB} @@ -216,12 +216,12 @@ widget_class mainView {open } } } - Fl_Group {subPanels_[kPanelSettings]} {open + Fl_Group {subPanels_[kPanelSettings]} {selected xywh {5 109 790 286} class LogicalGroup } { Fl_Group {} { - label Engine open selected + label Engine open xywh {260 135 280 100} box ROUNDED_BOX labelsize 12 align 17 class TitleGroup } { @@ -279,7 +279,7 @@ widget_class mainView {open Fl_Dial stretchedTuningSlider_ { comment {tag=kTagSetStretchedTuning} xywh {515 315 48 48} value 0.5 - class Knob48 + class StyledKnob } Fl_Box {} { label Stretch diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 752601f0..ba16d43f 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -335,6 +335,9 @@ void Editor::Impl::createFrameContents() CColor iconHighlight; CColor valueText; CColor valueBackground; + CColor knobActiveTrackColor; + CColor knobInactiveTrackColor; + CColor knobLineIndicatorColor; }; Theme lightTheme; @@ -346,6 +349,9 @@ void Editor::Impl::createFrameContents() lightTheme.iconHighlight = { 0xa8, 0x62, 0x34 }; lightTheme.valueText = { 0xff, 0xff, 0xff }; lightTheme.valueBackground = { 0x2e, 0x34, 0x36 }; + lightTheme.knobActiveTrackColor = { 0x00, 0xb6, 0x2a }; + lightTheme.knobInactiveTrackColor = { 0x30, 0x30, 0x30 }; + lightTheme.knobLineIndicatorColor = { 0x00, 0x00, 0x00 }; Theme darkTheme; darkTheme.boxBackground = { 0x2e, 0x34, 0x36 }; darkTheme.text = { 0xff, 0xff, 0xff }; @@ -355,6 +361,9 @@ void Editor::Impl::createFrameContents() darkTheme.iconHighlight = { 0xa8, 0x62, 0x34 }; darkTheme.valueText = { 0x2e, 0x34, 0x36 }; darkTheme.valueBackground = { 0xff, 0xff, 0xff }; + darkTheme.knobActiveTrackColor = { 0x00, 0xb6, 0x2a }; + darkTheme.knobInactiveTrackColor = { 0x60, 0x60, 0x60 }; + darkTheme.knobLineIndicatorColor = { 0xff, 0xff, 0xff }; Theme& defaultTheme = lightTheme; Theme* theme = &defaultTheme; @@ -367,6 +376,7 @@ void Editor::Impl::createFrameContents() typedef CTextLabel Label; typedef CViewContainer HLine; typedef CAnimKnob Knob48; + typedef SStyledKnob StyledKnob; typedef CTextLabel ValueLabel; typedef CViewContainer VMeter; typedef SValueMenu ValueMenu; @@ -426,6 +436,13 @@ void Editor::Impl::createFrameContents() auto createKnob48 = [this, &knob48](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int) { return new CAnimKnob(bounds, this, tag, 31, 48, knob48); }; + auto createStyledKnob = [this, &theme](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int) { + SStyledKnob* knob = new SStyledKnob(bounds, this, tag); + knob->setActiveTrackColor(theme->knobActiveTrackColor); + knob->setInactiveTrackColor(theme->knobInactiveTrackColor); + knob->setLineIndicatorColor(theme->knobLineIndicatorColor); + return knob; + }; auto createValueLabel = [&theme](const CRect& bounds, int, const char* label, CHoriTxtAlign align, int fontsize) { CTextLabel* lbl = new CTextLabel(bounds, label); lbl->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index be9899ef..d3e11b02 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "GUIComponents.h" +#include #include #include "utility/vstgui_before.h" @@ -428,6 +429,7 @@ void SValueMenu::onItemClicked(int32_t index) valueChanged(); } +/// void STextButton::setHoverColor (const CColor& color) { hoverColor_ = color; @@ -458,3 +460,93 @@ CMouseEventResult STextButton::onMouseExited (CPoint& where, const CButtonState& setDirty(); return CTextButton::onMouseExited(where, buttons); } + +/// +SStyledKnob::SStyledKnob(const CRect& size, IControlListener* listener, int32_t tag) + : CKnobBase(size, listener, tag, nullptr) +{ +} + +void SStyledKnob::setActiveTrackColor(const CColor& color) +{ + if (activeTrackColor_ == color) + return; + activeTrackColor_ = color; + setDirty(); +} + +void SStyledKnob::setInactiveTrackColor(const CColor& color) +{ + if (inactiveTrackColor_ == color) + return; + inactiveTrackColor_ = color; + setDirty(); +} + +void SStyledKnob::setLineIndicatorColor(const CColor& color) +{ + if (lineIndicatorColor_ == color) + return; + lineIndicatorColor_ = color; + setDirty(); +} + +void SStyledKnob::draw(CDrawContext* dc) +{ + const CCoord lineWidth = 4.0; + const CCoord indicatorLineLength = 10.0; + const CCoord angleSpread = 250.0; + const CCoord angle1 = 270.0 - 0.5 * angleSpread; + const CCoord angle2 = 270.0 + 0.5 * angleSpread; + + dc->setDrawMode(kAntiAliasing); + + const CRect bounds = getViewSize(); + + // compute inner bounds + CRect rect(bounds); + rect.setWidth(std::min(rect.getWidth(), rect.getHeight())); + rect.setHeight(rect.getWidth()); + rect.centerInside(bounds); + rect.extend(-lineWidth, -lineWidth); + + SharedPointer path; + + // inactive track + path = owned(dc->createGraphicsPath()); + path->addArc(rect, angle1, angle2, true); + + dc->setFrameColor(inactiveTrackColor_); + dc->setLineWidth(lineWidth); + dc->setLineStyle(kLineSolid); + dc->drawGraphicsPath(path, CDrawContext::kPathStroked); + + // active track + const CCoord v = getValueNormalized(); + const CCoord vAngle = angle1 + v * angleSpread; + path = owned(dc->createGraphicsPath()); + path->addArc(rect, angle1, vAngle, true); + + dc->setFrameColor(activeTrackColor_); + dc->setLineWidth(lineWidth + 0.5); + dc->setLineStyle(kLineSolid); + dc->drawGraphicsPath(path, CDrawContext::kPathStroked); + + // indicator line + { + CCoord module1 = 0.5 * rect.getWidth() - indicatorLineLength; + CCoord module2 = 0.5 * rect.getWidth(); + std::complex c1 = std::polar(module1, vAngle * (M_PI / 180.0)); + std::complex c2 = std::polar(module2, vAngle * (M_PI / 180.0)); + + CPoint p1(c1.real(), c1.imag()); + CPoint p2(c2.real(), c2.imag()); + p1.offset(rect.getCenter()); + p2.offset(rect.getCenter()); + + dc->setFrameColor(lineIndicatorColor_); + dc->setLineWidth(1.0); + dc->setLineStyle(kLineSolid); + dc->drawLine(p1, p2); + } +} diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index e0f3b173..c9afccba 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -162,3 +162,25 @@ private: CColor hoverColor_; bool hovered { false }; }; + +/// +class SStyledKnob : public CKnobBase { +public: + SStyledKnob(const CRect& size, IControlListener* listener, int32_t tag); + + const CColor& getActiveTrackColor() const { return activeTrackColor_; } + void setActiveTrackColor(const CColor& color); + const CColor& getInactiveTrackColor() const { return inactiveTrackColor_; } + void setInactiveTrackColor(const CColor& color); + const CColor& getLineIndicatorColor() const { return lineIndicatorColor_; } + void setLineIndicatorColor(const CColor& color); + + CLASS_METHODS(SStyledKnob, CKnobBase) +protected: + void draw(CDrawContext* dc) override; + +private: + CColor activeTrackColor_; + CColor inactiveTrackColor_; + CColor lineIndicatorColor_; +}; diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index 18a413a5..af348d36 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -58,7 +58,7 @@ view__24->setVisible(false); ValueLabel* const view__25 = createValueLabel(CRect(40, 65, 100, 70), -1, "Center", kCenterText, 12); view__23->addView(view__25); view__25->setVisible(false); -Knob48* const view__26 = createKnob48(CRect(110, 15, 158, 63), kTagSetVolume, "", kCenterText, 14); +StyledKnob* const view__26 = createStyledKnob(CRect(110, 15, 158, 63), kTagSetVolume, "", kCenterText, 14); volumeSlider_ = view__26; view__23->addView(view__26); ValueLabel* const view__27 = createValueLabel(CRect(105, 65, 165, 87), -1, "0.0 dB", kCenterText, 12); @@ -135,7 +135,7 @@ tuningFrequencySlider_ = view__54; view__52->addView(view__54); ValueLabel* const view__55 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); view__52->addView(view__55); -Knob48* const view__56 = createKnob48(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); +StyledKnob* const view__56 = createStyledKnob(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); stretchedTuningSlider_ = view__56; view__52->addView(view__56); ValueLabel* const view__57 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); From 19540e821aca42ff3325ef979c0e0a21ee41f8d8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 15 Sep 2020 22:23:42 +0200 Subject: [PATCH 248/445] Add a circular test case for the curve --- tests/CurveT.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index 4c3c9f9d..68a88b9c 100644 --- a/tests/CurveT.cpp +++ b/tests/CurveT.cpp @@ -9,6 +9,7 @@ #include "catch2/catch.hpp" #include using namespace Catch::literals; +using namespace sfz::literals; TEST_CASE("[Curve] Bipolar 0 to 1") { @@ -242,3 +243,20 @@ TEST_CASE("[Curve] Default CurveSet") REQUIRE( curveSet.getCurve(6).evalNormalized(1.0f) == 0.0f ); REQUIRE( curveSet.getCurve(6).evalNormalized(0.3f) == Approx(0.837).margin(1e-3) ); } + +TEST_CASE("[Curve] Build from points") +{ + std::array curvePoints; + float val = 0.0f; + float step = 1 / static_cast(sfz::Curve::NumValues); + for (auto& x : curvePoints) { + x = val; + val += step; + } + + sfz::Curve curve = sfz::Curve::buildFromPoints(curvePoints.data()); + REQUIRE(curve.evalNormalized(0.0) == curvePoints[0]); + REQUIRE(curve.evalNormalized(1.0) == curvePoints[sfz::Curve::NumValues - 1]); + REQUIRE(curve.evalNormalized(63_norm) == curvePoints[63]); +} + From 4d87716a305e39723836553a81efade0b1d64e93 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 08:41:02 +0200 Subject: [PATCH 249/445] Offset the time so that the envelopes "end" on the correct value The envelope start is always considered to be the value before the current ramp started --- src/sfizz/FlexEnvelope.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index 321093fc..d56aad25 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -165,11 +165,11 @@ void FlexEnvelope::Impl::process(absl::Span out) const Curve& curve = *stageCurve_; size_t framesDone = 0; while ((time < stageEndTime || sustained) && frameIndex < maxFrameIndex) { + time += samplePeriod; float x = time * (1.0f / stageEndTime); float c = curve.evalNormalized(x); level = sourceLevel + c * (targetLevel - sourceLevel); out[frameIndex++] = level; - time += samplePeriod; ++framesDone; } currentLevel_ = level; From f969761b5abc5263b226fe8ab25f758641ec7fa5 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 08:45:29 +0200 Subject: [PATCH 250/445] Only zero the remaining samples when the envelope is finished --- src/sfizz/FlexEnvelope.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index d56aad25..70dc0948 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -145,6 +145,7 @@ void FlexEnvelope::Impl::process(absl::Span out) while ((!stageSustained_ && currentTime_ >= stageTime_) || (stageSustained_ && isReleased)) { if (!advanceToNextStage()) { + out.remove_prefix(frameIndex); fill(out, 0.0f); return; } From f317e2f6b9679d5fa23c3ec44e3979ac5218b7e6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 09:03:39 +0200 Subject: [PATCH 251/445] Add flex eg tests, move test helpers around, and add a more verbose mod matrix graph output --- src/sfizz/modulations/ModKey.cpp | 16 +- tests/ADSREnvelopeT.cpp | 16 +- tests/CMakeLists.txt | 1 + tests/FlexEGT.cpp | 263 +++++++++++++++++++++++++++++++ tests/ModulationsT.cpp | 35 +--- tests/TestHelpers.cpp | 31 ++++ tests/TestHelpers.h | 21 +++ 7 files changed, 330 insertions(+), 53 deletions(-) create mode 100644 tests/FlexEGT.cpp diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index c5ee2a37..9012fb46 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -71,22 +71,22 @@ std::string ModKey::toString() const " {curve=", params_.curve, ", smooth=", params_.smooth, ", value=", params_.value, ", step=", params_.step, "}"); case ModId::Envelope: - return absl::StrCat("EG ", 1 + params_.N); + return absl::StrCat("EG ", 1 + params_.N, " {region=", region_.number(), "}"); case ModId::LFO: - return absl::StrCat("LFO ", 1 + params_.N); + return absl::StrCat("LFO ", 1 + params_.N, " {region=", region_.number(), "}"); case ModId::Amplitude: - return "Amplitude"; + return absl::StrCat("Amplitude", " {region=", region_.number(), "}"); case ModId::Pan: - return "Pan"; + return absl::StrCat("Pan", " {region=", region_.number(), "}"); case ModId::Width: - return "Width"; + return absl::StrCat("Width", " {region=", region_.number(), "}"); case ModId::Position: - return "Position"; + return absl::StrCat("Position", " {region=", region_.number(), "}"); case ModId::Pitch: - return "Pitch"; + return absl::StrCat("Pitch", " {region=", region_.number(), "}"); case ModId::Volume: - return "Volume"; + return absl::StrCat("Volume", " {region=", region_.number(), "}"); default: return {}; diff --git a/tests/ADSREnvelopeT.cpp b/tests/ADSREnvelopeT.cpp index df104407..424d8273 100644 --- a/tests/ADSREnvelopeT.cpp +++ b/tests/ADSREnvelopeT.cpp @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "sfizz/ADSREnvelope.h" +#include "TestHelpers.h" #include "catch2/catch.hpp" #include #include @@ -13,21 +14,6 @@ #include using namespace Catch::literals; -template -inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) -{ - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0; i < rhs.size(); ++i) - if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; - } - - return true; -} - TEST_CASE("[ADSREnvelope] Basic state") { sfz::ADSREnvelope envelope; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 61b16360..0090620c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(SFIZZ_TEST_SOURCES # If we're tweaking the curves this kind of tests does not make sense # Use integration tests with comparison curves # ADSREnvelopeT.cpp + FlexEGT.cpp EventEnvelopesT.cpp MainT.cpp SynthT.cpp diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp new file mode 100644 index 00000000..f48e5455 --- /dev/null +++ b/tests/FlexEGT.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + + +#include "sfizz/Synth.h" +#include "sfizz/FlexEnvelope.h" +#include "catch2/catch.hpp" +#include "TestHelpers.h" +using namespace Catch::literals; +using namespace sfz::literals; + +TEST_CASE("[FlexEG] Values") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_amplitude=1 + eg1_time1=.1 eg1_level1=.25 + eg1_time2=.2 eg1_level2=1 + eg1_time3=.2 eg1_level3=.5 eg1_sustain=3 + eg1_time4=.4 eg1_level4=1 + )"); + REQUIRE(synth.getNumRegions() == 1); + const auto* region = synth.getRegionView(0); + REQUIRE( region->flexEGs.size() == 1 ); + const auto& egDescription = region->flexEGs[0]; + REQUIRE( egDescription.points.size() == 5 ); + REQUIRE( egDescription.points[0].time == 0.0_a ); + REQUIRE( egDescription.points[0].level == 0.0_a ); + REQUIRE( egDescription.points[1].time == .1_a ); + REQUIRE( egDescription.points[1].level == .25_a ); + REQUIRE( egDescription.points[2].time == .2_a ); + REQUIRE( egDescription.points[2].level == 1.0_a ); + REQUIRE( egDescription.points[3].time == .2_a ); + REQUIRE( egDescription.points[3].level == .5_a ); + REQUIRE( egDescription.points[4].time == .4_a ); + REQUIRE( egDescription.points[4].level == 1.0_a ); + REQUIRE( egDescription.sustain == 3 ); + REQUIRE(synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + })); +} + +TEST_CASE("[FlexEG] Default values") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg3_time2=.1 eg3_level2=.25 + )"); + REQUIRE(synth.getNumRegions() == 1); + const auto* region = synth.getRegionView(0); + REQUIRE( region->flexEGs.size() == 3 ); + REQUIRE( region->flexEGs[0].points.size() == 0 ); + REQUIRE( region->flexEGs[1].points.size() == 0 ); + const auto& egDescription = region->flexEGs[2]; + REQUIRE( egDescription.points.size() == 3 ); + REQUIRE( egDescription.points[0].time == 0.0_a ); + REQUIRE( egDescription.points[0].level == 0.0_a ); + REQUIRE( egDescription.points[1].time == 0.0_a ); + REQUIRE( egDescription.points[1].level == 0.0_a ); + REQUIRE( egDescription.points[2].time == .1_a ); + REQUIRE( egDescription.points[2].level == .25_a ); + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({}) ); +} + +TEST_CASE("[FlexEG] Connections") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine eg1_amplitude=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_pan=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_width=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_position=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_pitch=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_volume=1 eg1_time1=.1 eg1_level1=.25 + )"); + REQUIRE(synth.getNumRegions() == 6); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + REQUIRE( synth.getRegionView(0)->flexEGs[0].points.size() == 2 ); + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + R"("EG 1 {region=1}" -> "Pan {region=1}")", + R"("EG 1 {region=2}" -> "Width {region=2}")", + R"("EG 1 {region=3}" -> "Position {region=3}")", + R"("EG 1 {region=4}" -> "Pitch {region=4}")", + R"("EG 1 {region=5}" -> "Volume {region=5}")", + }, 6)); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (No release)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(16); + envelope.start(1); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { 0.0f, 0.05f, 0.1f, 0.15f, 0.2f, 0.25f, 0.4f, 0.55f, 0.7f, 0.85f, 1.0f, 1.0f, 1.0f }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (with release)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(32); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining + REQUIRE( output[16] == 0.0_a ); // released + REQUIRE( output[31] == 0.0_a ); // released +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test (with release and release ramp)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_time3=0.5 eg1_level3=0 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { + 0.0f, + 0.05f, 0.1f, 0.15f, 0.2f, 0.25f, + 0.4f, 0.55f, 0.7f, 0.85f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.8f, 0.6f, 0.4f, 0.2f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (with shapes)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 eg1_shape1=2 + eg1_time2=0.5 eg1_level2=1 eg1_shape2=0.5 + eg1_sustain=2 + eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(32); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining + REQUIRE( output[31] == 0.0_a ); // released +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test (with shapes)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 eg1_shape1=2 + eg1_time2=0.5 eg1_level2=1 eg1_shape2=0.5 + eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { + 0.0f, + 0.01f, 0.04f, 0.09f, 0.16f, 0.25f, + 0.58f, 0.72f, 0.83f, 0.92f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.99f, 0.97f, 0.87f, 0.59f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected, 0.01f) ); +} diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 48203a2e..627714d6 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -7,6 +7,7 @@ #include "sfizz/modulations/ModId.h" #include "sfizz/modulations/ModKey.h" #include "sfizz/Synth.h" +#include "TestHelpers.h" #include "catch2/catch.hpp" TEST_CASE("[Modulations] Identifiers") @@ -78,32 +79,6 @@ TEST_CASE("[Modulations] Display names") }); } -static std::string createReferenceGraph(std::vector lines) -{ - const char* defaultConnections[] = { - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude")", - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan")" - }; - - for (const char* line : defaultConnections) - lines.push_back(line); - - std::sort(lines.begin(), lines.end()); - - std::string graph; - graph.reserve(1024); - - graph += "digraph {\n"; - for (const std::string& line : lines) { - graph.push_back('\t'); - graph += line; - graph.push_back('\n'); - } - graph += "}\n"; - - return graph; -}; - TEST_CASE("[Modulations] Connection graph from SFZ") { sfz::Synth synth; @@ -118,9 +93,9 @@ width_oncc425=29 const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude")", - R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch")", - R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan")", - R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width")", + R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {region=0}")", + R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {region=0}")", + R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {region=0}")", + R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {region=0}")", })); } diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index f58eee73..fb4bd233 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -68,3 +68,34 @@ unsigned numPlayingVoices(const sfz::Synth& synth) return !v->releasedOrFree(); }); } + +std::string createReferenceGraph(std::vector lines, int numRegions) +{ + for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { + lines.push_back(absl::StrCat( + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {region=)", + regionIdx, + R"(}")" + )); + lines.push_back(absl::StrCat( + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {region=)", + regionIdx, + R"(}")" + )); + } + + std::sort(lines.begin(), lines.end()); + + std::string graph; + graph.reserve(1024); + + graph += "digraph {\n"; + for (const std::string& line : lines) { + graph.push_back('\t'); + graph += line; + graph.push_back('\n'); + } + graph += "}\n"; + + return graph; +}; diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index e5e48671..272fb612 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -65,3 +65,24 @@ const std::vector getPlayingVoices(const sfz::Synth& synth); * @return unsigned */ unsigned numPlayingVoices(const sfz::Synth& synth); + +/** + * @brief Create the dot graph representation from a list of strings + * + */ +std::string createReferenceGraph(std::vector lines, int numRegions = 1); + +template +inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) +{ + if (lhs.size() != rhs.size()) + return false; + + for (size_t i = 0; i < rhs.size(); ++i) + if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { + std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; + return false; + } + + return true; +} From 8ec3f3d2a882cba2087d9135b7bb1c3787e981bd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 10:41:55 +0200 Subject: [PATCH 252/445] midnam: only emit CC which are in 7bit range --- src/sfizz/Synth.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index beae4e64..a054687f 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1257,13 +1257,15 @@ std::string sfz::Synth::exportMidnam(absl::string_view model) const cns.append_attribute("Name").set_value("Controls"); for (const auto& pair : ccLabels) { anonymousCCs.set(pair.first, false); - pugi::xml_node cn = cns.append_child("Control"); - cn.append_attribute("Type").set_value("7bit"); - cn.append_attribute("Number").set_value(std::to_string(pair.first).c_str()); - cn.append_attribute("Name").set_value(pair.second.c_str()); + if (pair.first < 128) { + pugi::xml_node cn = cns.append_child("Control"); + cn.append_attribute("Type").set_value("7bit"); + cn.append_attribute("Number").set_value(std::to_string(pair.first).c_str()); + cn.append_attribute("Name").set_value(pair.second.c_str()); + } } - for (unsigned i = 0; i < anonymousCCs.size(); ++i) { + for (unsigned i = 0, n = std::min(128, anonymousCCs.size()); i < n; ++i) { if (anonymousCCs[i]) { pugi::xml_node cn = cns.append_child("Control"); cn.append_attribute("Type").set_value("7bit"); From 1b104fd6d66bd6e5e1bba6795900ac1a624f24f1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 10:46:54 +0200 Subject: [PATCH 253/445] Initialize CC labels for volume and pan --- src/sfizz/Synth.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index a054687f..32ceccad 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -231,6 +231,10 @@ void sfz::Synth::clear() // set default controllers cc(0, 7, 100); // volume hdcc(0, 10, 0.5f); // pan + + // set default controller labels + ccLabels.emplace_back(7, "Volume"); + ccLabels.emplace_back(10, "Pan"); } void sfz::Synth::handleMasterOpcodes(const std::vector& members) From 111af1b6488fcd9b28ae1940c7fcf684480b736d Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 11:13:37 +0200 Subject: [PATCH 254/445] Ensure label entries to be unique for their cc/note --- src/sfizz/SfzHelpers.h | 29 +++++++++++++++++++++++++++++ src/sfizz/Synth.cpp | 10 +++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index 31ea49c8..416b2304 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -7,6 +7,7 @@ #pragma once #include #include +#include //#include #include #include @@ -202,6 +203,34 @@ inline CXX14_CONSTEXPR Type vaGain(Type cutoff, Type sampleRate) return std::tan(cutoff / sampleRate * pi()); } +/** + * @brief Insert an item uniquely into a vector of pairs. + * + * @param pairVector the vector of pairs + * @param key the unique key + * @param value the value + * @param replace whether to replace the value if the key is already present + * @return whether the item was inserted + */ +template +bool insertPairUniquely(std::vector
- - - - - diff --git a/doxygen/layout/custom_header.html b/doxygen/layout/custom_header.html deleted file mode 100644 index bf5a07f6..00000000 --- a/doxygen/layout/custom_header.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - Home - sfizz - - - - - - - - - - - - - - - - - - - - - -$treeview -$search -$mathjax - -$extrastylesheet - - - - - - - - -
- -
diff --git a/doxygen/layout/extra_stylesheet.css b/doxygen/layout/extra_stylesheet.css deleted file mode 100644 index 116008da..00000000 --- a/doxygen/layout/extra_stylesheet.css +++ /dev/null @@ -1,198 +0,0 @@ -#page_container { - position: relative; - margin: 0; - padding: 0; - height: auto !important; - height: 100%; - min-height: 100%; -} - -div.contents, div.searchresults { - margin-top: 10px; - margin-right: 12px; - padding-bottom: 70px; -} - -#projectlogo { - text-align: left; -} - -#projectnumber { - font-size: 120%; - font-family: Tahoma, Arial, sans-serif; - text-align: right; - padding: 0.5em 1em; -} - -.tabs { - font-size: 14px; -} -.tabs2, .tabs3 { - font-size: 12px; -} - -.navpath ul { - font-size: 12px; -} - -h1, h2, h3, h4, h5, h6 { - color: #002D88; - font-weight: normal; - margin-top: 1em; - margin-bottom: 0.5em; - padding-top: 8px; - padding-bottom: 4px; - width: 100%; -} - -h1 { - font-size: 150%; - border-bottom: 1px solid #3276FF; -} -h2 { - font-size: 135%; - margin-top: 0.75em; -} -h3 { - font-size: 120%; - margin-top: 0.5em; -} -h4 { - font-size: 100%; - margin-top: 0.5em; -} - -div.headertitle h1 { - margin: 10px 2px; - border: none; - padding: 0; - width: auto; - color: black; - font-weight: bold; -} - -div.toc h3 { - font-size: 14px; -} - -div.toc li { - font-size: 12px; - line-height: 1.3; - padding-left: 14px; -} - -img.logo { - float: right; - margin: 20px; -} - -div.logo { - float: right; - margin: 20px; -} - -table.directory { - line-height: 1.5; -} - -.icon { - line-height: 1.25; -} - -div.appearance { - margin: 1em 0em; -} -div.appearance table { - margin: 0.5em 0em; - width: 100%; - text-align: center; -} -div.appearance img { - margin: 0.5em; -} -div.appearance .caption { - font-style: italic; - font-weight: normal; - font-size: 90%; -} - -div.appearance_brief table { - width: 100%; - table-layout: fixed; - text-align: center; - border-collapse: collapse; -} - -div.appearance_brief table td:first-child { - width: 20em; - text-align: left; - padding-left: 2em; -} - -div.appearance_brief table td { - border-style: none solid solid none; - border-width: 1px; - border-color: lightblue; -} - - -td.green { color: green; } -td.orange { color: #ff8000; } -td.red { color: red; } - -span.literal { - text-decoration: none; - font-weight: bold; - font-family: monospace, fixed; -} - -/* we make all the following tags render the text just like - the standard Doxygen @remarks, @see tags do, to obtain a uniform - look and feel */ -span.itemdef, span.lib, span.category, span.stdobj, span.styles, -span.events, span.flags, span.appearance, span.impl, span.avail { - font-weight: bold; - line-height: 130%; -} - -span.style, span.event, span.flag { - font-weight: bold; - color: #880000; -} - -div.styleDesc, div.eventDesc, div.flagDesc { - margin-left: 3%; - margin-bottom: 1ex; -} - -div.eventHandler { - margin: 1em; - text-indent: 3%; -} - -div.eventHandler span { - padding: 5px; - background-color: #eeeeee; - font-family: monospace, fixed; -} - -code { - font-size: 110%; - color: #444444; -} - -address.footer { - position: absolute; - bottom: 0; - margin: 0; - padding: 10px 0; - width: 100%; - border-top: 1px solid #0043CC; - background-image: url('nav_h.png'); - background-repeat: repeat-x; - background-color: #F4F8FF; -} - -address.footer small { - padding: 0 10px; -} diff --git a/doxygen/pages/engine_description.md b/doxygen/pages/engine_description.md deleted file mode 100644 index ab4905e9..00000000 --- a/doxygen/pages/engine_description.md +++ /dev/null @@ -1,154 +0,0 @@ -# Global view of the engine - -The sfizz engine is basically a "Synth" object that takes an SFZ file in, receives MIDI-type events -and is able to render audio through successive calls to a callback function. This is in line with -the way most audio applications and plugins are working. A high-level overview is presented in the -following diagram. - -``` - C and C++ API entry point - - | - | - | - | - +--------v-------+ - | | - +-------------------------- Synth -----------------------------+ - | | | | - | +----------------+ | - | | | - +--------v------+ | +---------v--------+ - | | +---------v---------+ | | - | Region list | | Common resources | | Voice pool | - | | | and state | | | - +---------------+ | ----------------- | +------------------+ - Built from the SFZ file | File pool | - | Envelope pool | The voices are the polyphony - Each region is a semi-passive | LFO pool | of the synth. They are idle - description object that can | Buffer pool | and they get activated by the - decide whether it is "active" | Midi state | synth to play a region on a - or not depending on the chain | ... | specific event. They are then - of MIDI events it receives. +-------------------+ "linked" to the region while - Once activated, a voice is There are a number of common it is played, and reset to - chosen to play the region until resources that are needed for their idle state when they - it ends naturally or through all the regions and in parti- are done playing the region. - note-offs or off-groups. cular the voices. This includes - all the (preloaded) files for - the SFZ instrument, but will - include in the future the EG - and LFOs that are needed to - achieve compliance with the - SFZ v2 specification. This will - also include a temporary buffer - holder that voices may share. - A common resource of importance - is the MIDI state: note durations - are needed for some opcodes -- - for example rt_decay -- and - triggering velocities too. -``` - -The Synth, Voices and Regions form the bulk of the code complexity. The rest of the engine is dedicated of -mostly helper classes to enable easy management of floating-point buffers in which the audio data is held, -signal processing and accelerated (SIMD) computations, and abstractions that are specific to the SFZ format -such as envelope generators, curves or LFOs. - -# Parsing the SFZ files - -The sfz file logic is pretty simple and well defined. The sfzformat.com website contains an extensive documentation -on it. At its core, an SFZ file describes a list of `region` objects on which a certain number of "opcodes" will -apply. Opcodes can determine the sample played, the event conditions that will trigger the sample such as the range -of notes, channels, velocities, the processing to apply on the sample while playing, and many more things. It is -also possible to describe a `group` of regions, as well as exclusive groups that will shut off other regions that may -already be playing. There are also `master` groups, and `global` opcodes and some other types. - -All the opcodes are declared within a header, in a pseudo-xml markup language that looks like this -```sfz - volume=6 - set_cc4=5 - key=36 sample=kick.wav -``` -Here we have 3 headers (`global`, `control` and `region`) and each header holds some opcodes. All of these opcodes -have a value---for example the volume is equal to 6 in the `global` header. Some opcodes also have parameters. -The `control` header holds an opcode `set_cc` with the parameter `4` and value `5`. The parameter here is the CC to set, -and the value at which to set it is 5. - -The parsing logic of sfizz is handled through a base class called Parser---a very original choice. This parser has -a virtual callback that gets called whenever a header description is "complete", along with a list of opcodes that -apply to the header. Subclassing the Parser then allows to build different SFZ handlers, from full-blown synths as -with sfizz to simpler things such as printers (see in particular https://github.com/sfztools/sfz-flat/). If we look -at the core of the latter example, it will look something like the following - -```cpp -class PrintingParser: public sfz::Parser -{ -protected: - void callback(absl::string_view header, const std::vector& members) final - { - switch (hash(header)) // The hash(...) function transforms strings to large integers - { - case hash("global"): // It is also compile-time defined, which allows to do switch-case - // statements on strings, something that is usually not possible - globalMembers = members; // We save the global headers since they apply to the next - // region (and groups and masters) - masterMembers.clear(); - groupMembers.clear(); - break; - case hash("master"): - masterMembers = members; // So on - groupMembers.clear(); - break; - case hash("group"): - groupMembers = members; // .. and so forth - break; - case hash("region"): - std::cout << "<" << header << ">" << ' '; // Now we print the region along with all the opcodes - // we memorized from earlier headers. - printMembers(globalMembers); - printMembers(masterMembers); - printMembers(groupMembers); - printMembers(members); - std::cout << '\n'; - break; - default: - std::cout << "<" << header << ">" << ' '; - printMembers(members); - std::cout << '\n'; - break; - } - } -private: - std::vector globalMembers; - std::vector masterMembers; - std::vector groupMembers; - void printMembers(const std::vector& members) - { - for (auto& member: members) - { - std::cout << member.opcode; - if (member.parameter) - std::cout << +*member.parameter; - std::cout << "=" << member.value; - std::cout << ' '; - } - } -}; -``` - -The main function is then quite straightforward and we call a function from the Parser class that loads a file -```cpp -PrintingParser parser; -parser.loadSfzFile("my_sfz_file.sfz"); -``` -If you circle back to the parser you will see that opcodes are stored in an `Opcode` class. This class does some parsing -itself and separates the opcode name itself, parameters if any, and the value. Opcodes are very cheap to copy and pass -around because they only refer to characters in the file that are stored inside the `Parser` class, so feel free to -create vectors of them and move them around. - -Note that you may also derive the loadSfzFile method if you have any processing you need to do before the actual parsing happens. - -# Building the region list in sfizz - -The callback method from sfizz is actually quite similar to the one shown above, except that instead of printing the region -we actually fill a big structure from it. diff --git a/doxygen/pages/index.md b/doxygen/pages/index.md deleted file mode 100644 index a7e12b3f..00000000 --- a/doxygen/pages/index.md +++ /dev/null @@ -1,6 +0,0 @@ -# sfizz - -SFZ file format library - -- [public C API](sfizz_8h.html) -- [public C++ API](classsfz_1_1_sfizz.html) diff --git a/doxygen/scripts/generate_api_index.sh b/doxygen/scripts/generate_api_index.sh deleted file mode 100755 index af618b6f..00000000 --- a/doxygen/scripts/generate_api_index.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# Must be called from the root directory -if [[ -f api_index.md ]]; then rm api_index.md; fi -cat >>api_index.md <> api_index.md -done diff --git a/doxygen/scripts/Doxyfile.in b/scripts/doxygen/Doxyfile.in similarity index 100% rename from doxygen/scripts/Doxyfile.in rename to scripts/doxygen/Doxyfile.in diff --git a/scripts/doxygen/doxy2json.py b/scripts/doxygen/doxy2json.py new file mode 100644 index 00000000..6fa25bb7 --- /dev/null +++ b/scripts/doxygen/doxy2json.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" + SPDX-License-Identifier: BSD-2-Clause + + This code is part of the sfizz library and is licensed under a BSD 2-clause + license. You should have receive a LICENSE.md file along with the code. + If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + + Converts the Doxygen XML output to a custom JSON structure. + + The JSON output will be used on a Jekyll website, + parsed by a related layout (_layouts/doxygen.html). + + Known bugs / wish list: + + - Replace `strip_tags_in_text()` and `tag_list_as_string()` with a recursive + parsing function able to extract also detailed descriptions paragraphs + (E.g. `sfizz_oversampling_factor_t` has 3), getting rid of various + `@since`, `@note` and `@return`, which are parsed separately. + It should also covert the Doxygen custom tags (like , see below, + or should this be done by some XSLT trasformation file?). + + - Tags in ALIASES list needs to be escaped (parsed by tag_list_as_string()) + to avoid Doxygen convert them in its own tag structure, by loosing some + details (e.g.: true becomes true). + + - Merge the work done previously to be able to fully automate the process + to be used in various CIs. +""" +import xml.etree.ElementTree as ET +import json + +# TODO: scan for files +tree = ET.parse('xml/classsfz_1_1_sfizz.xml') +root = tree.getroot() +data = {} + +def strip_tags_in_text(element): + if element is None: + return "" + + returned_string = element.text or "" + for t in element: + if t.tag == "ref": + returned_string += "{}".format(t.text.replace("()", ''), t.text) + returned_string += t.tail or "" + elif t.tag == "computeroutput": + returned_string += "{}".format(t.text) + returned_string += t.tail or "" + else: + returned_string += t.text or "" + returned_string += t.tail or "" + + return returned_string.strip() + +def tag_list_as_string(element_list): + if element_list is None or len(element_list) == 0: + return "" + + if element_list[0].text is not None: + return element_list[0].text.strip() + + returned_string = "" + for element in element_list: + for t in element: +# if t.tag == "bold": t.tag = 'b' + returned_string += ET.tostring(t, encoding="unicode") + + return returned_string.strip() + +name = root.find("./compounddef/compoundname") +kind = root.find("./compounddef").get("kind") +brief = root.find("./compounddef/briefdescription/para") +include = root.find("./compounddef/includes") +location = root.find("./compounddef/location").get("file") +language = root.find("./compounddef").get("language") +version = root.get("version") + +if name is not None and name.text is not None: data["name"] = name.text +if kind is not None: data["kind"] = kind +if brief is not None and brief.text is not None: data["brief"] = brief.text.strip() +if include is not None and include.text is not None: data["include"] = include.text.strip() +if location is not None: data["location"] = location +if language is not None: data["language"] = language +if version is not None: data["doxygen_version"] = version +definitions = [] + +for sectiondef in root.iter("sectiondef"): + + def_kind = sectiondef.get("kind") + definition = {} + members = [] + definition["kind"] = def_kind + + for memberdef in sectiondef: + + member = {} + member["name"] = memberdef.find("name").text + + type_member = memberdef.find("type") + initializer_member = memberdef.find("initializer") + brief_member = memberdef.find("briefdescription/para") + description_member = memberdef.find("detaileddescription/para") + return_member = memberdef.findall("detaileddescription/para/simplesect[@kind='return']/para") + since_member = memberdef.find("detaileddescription/para/simplesect[@kind='since']/para") + note_member = memberdef.find("detaileddescription/para/simplesect[@kind='note']/para") + + if type_member is not None: + type_member = strip_tags_in_text(type_member) + if type_member != '': + member["type"] = type_member + + if initializer_member is not None and initializer_member.text is not None: + member["initializer"] = initializer_member.text + + if brief_member is not None: + member["brief"] = strip_tags_in_text(brief_member) + + if description_member is not None: + description_member = strip_tags_in_text(description_member) + if description_member != '': + member["description"] = description_member + + if return_member is not None: + return_member = tag_list_as_string(return_member) + if return_member != '': + member["return"] = return_member + + if since_member is not None and since_member.text is not None: + member["since"] = since_member.text.strip() + + if note_member is not None and note_member.text is not None: + member["note"] = strip_tags_in_text(note_member) + + members.append(member) + + if def_kind == "enum" or def_kind == "public-type": + enumvalues = [] + for enumvalue in memberdef.iter("enumvalue"): + enum = {} + enum["name"] = enumvalue.find("name").text + + initializer_member = enumvalue.find("initializer") + brief_member = enumvalue.find("briefdescription/para") + description_member = enumvalue.find("detaileddescription/para") + + if initializer_member is not None and initializer_member.text is not None: + enum["initializer"] = initializer_member.text + + if brief_member is not None: + enum["brief"] = strip_tags_in_text(brief_member) + + if description_member is not None: + enum["description"] = strip_tags_in_text(description_member) + + enumvalues.append(enum) + + member["values"] = enumvalues + + params = [] + for paramtag in memberdef.findall("param"): + + param = {} + param["name"] = paramtag.find("declname").text + param["type"] = strip_tags_in_text(paramtag.find("type")) + + param_items = memberdef.findall("detaileddescription/para/parameterlist[@kind='param']/parameteritem") + for param_item in param_items: + param_name = param_item.find("parameternamelist/parametername").text + if param_name == param.get("name"): + description = param_item.find("parameterdescription/para") + if description is not None: + param["description"] = strip_tags_in_text(description) + + params.append(param) + + if params: + member["params"] = params + + definition["members"] = members + definitions.append(definition) + +data["definitions"] = definitions + +print(json.dumps(data, indent=2)) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 11d48265..7ffd3c1f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -245,7 +245,7 @@ if(WIN32) configure_file (${PROJECT_SOURCE_DIR}/scripts/innosetup.iss.in ${PROJECT_BINARY_DIR}/innosetup.iss @ONLY) endif() -configure_file (${PROJECT_SOURCE_DIR}/doxygen/scripts/Doxyfile.in ${PROJECT_SOURCE_DIR}/Doxyfile @ONLY) +configure_file (${PROJECT_SOURCE_DIR}/scripts/doxygen/Doxyfile.in ${PROJECT_SOURCE_DIR}/Doxyfile @ONLY) add_library (sfizz::parser ALIAS sfizz_parser) add_library (sfizz::sfizz ALIAS sfizz_static) From 9d0ee6969c457bb256dbb5e0d6274097e497459c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 28 Sep 2020 21:49:20 +0200 Subject: [PATCH 341/445] flexEG: allow release to bypass the pre-sustain stages It makes the EG equivalent to ARIA in case of early release. --- src/sfizz/FlexEnvelope.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index c9302c0b..3a7402dd 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -160,12 +160,24 @@ void FlexEnvelope::Impl::process(absl::Span out) } // Perform stage transitions - const bool isReleased = isReleased_; - while ((!stageSustained_ && currentTime_ >= stageTime_) || - (stageSustained_ && isReleased)) { - // If stage is of zero duration, immediate transition to level - if (!stageSustained_ && stageTime_ == 0) + if (isReleased_) { + // on release, fast forward past the sustain stage + const unsigned sustainStage = desc.sustain; + while (currentStageNumber_ <= sustainStage) { + if (!advanceToNextStage()) { + out.remove_prefix(frameIndex); + fill(out, 0.0f); + return; + } + } + } + while (!stageSustained_ && currentTime_ >= stageTime_) { + // advance through completed timed stages + ASSERT(isReleased_ || !stageSustained_); + if (stageTime_ == 0) { + // if stage is of zero duration, immediate transition to level currentLevel_ = stageTargetLevel_; + } if (!advanceToNextStage()) { out.remove_prefix(frameIndex); fill(out, 0.0f); From 9cab74f2c43aeb40100e986aad4a97a875f317c8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 28 Sep 2020 21:49:20 +0200 Subject: [PATCH 342/445] flexEG: allow release to bypass the pre-sustain stages It makes the EG equivalent to ARIA in case of early release. --- src/sfizz/FlexEnvelope.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index c58d3ae5..6fd67ac1 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -141,12 +141,24 @@ void FlexEnvelope::Impl::process(absl::Span out) } // Perform stage transitions - const bool isReleased = isReleased_; - while ((!stageSustained_ && currentTime_ >= stageTime_) || - (stageSustained_ && isReleased)) { - // If stage is of zero duration, immediate transition to level - if (!stageSustained_ && stageTime_ == 0) + if (isReleased_) { + // on release, fast forward past the sustain stage + const unsigned sustainStage = desc.sustain; + while (currentStageNumber_ <= sustainStage) { + if (!advanceToNextStage()) { + out.remove_prefix(frameIndex); + fill(out, 0.0f); + return; + } + } + } + while (!stageSustained_ && currentTime_ >= stageTime_) { + // advance through completed timed stages + ASSERT(isReleased_ || !stageSustained_); + if (stageTime_ == 0) { + // if stage is of zero duration, immediate transition to level currentLevel_ = stageTargetLevel_; + } if (!advanceToNextStage()) { out.remove_prefix(frameIndex); fill(out, 0.0f); From 2a26682543ae00ef7c1cd339f6a8ae63e38e078e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 28 Sep 2020 22:37:46 +0200 Subject: [PATCH 343/445] Add test for flexEG release --- tests/FlexEGT.cpp | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp index 229bee47..6490e30f 100644 --- a/tests/FlexEGT.cpp +++ b/tests/FlexEGT.cpp @@ -288,3 +288,72 @@ TEST_CASE("[FlexEG] Zero delay transitions") // Note(jpc): 0.9 is because EG pre-increments the time counter, slope is // 1 frame off into the future } + +TEST_CASE("[FlexEG] Early release") +{ + for (int i = 0; i < 3; ++i) { + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_ampeg=1 + eg1_time1=1.0 eg1_level1=1.0 + eg1_time2=1.0 eg1_level2=1.0 eg1_sustain=2 + eg1_time3=1.0 eg1_level3=0.0 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE(synth.getRegionView(0)->flexEGs.size() == 1); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + envelope.setSampleRate(100); + + envelope.start(0); + switch (i) { + case 0: + // A normal release: up 1s, sustain 1s, down 1s + envelope.release(200); + break; + case 1: + // A fast release: up 1s, down 1s + envelope.release(100); + break; + case 2: + // A faster release: up 0.5s, down 0.5s + envelope.release(50); + break; + } + + std::array output; + envelope.process(absl::MakeSpan(output)); + + // Theoretical output at 0.5s interval + const std::array ref0 {{ 0.0, 0.5, 1.0, 1.0, 1.0, 0.5, 0.0 }}; + const std::array ref1 {{ 0.0, 0.5, 1.0, 0.5, 0.0 }}; + const std::array ref2 {{ 0.0, 0.5, 0.25 }}; + + const float m = 0.015f; + switch (i) { + case 0: + REQUIRE(output[ 0] == Approx(ref0[0]).margin(m)); + REQUIRE(output[ 50] == Approx(ref0[1]).margin(m)); + REQUIRE(output[100] == Approx(ref0[2]).margin(m)); + REQUIRE(output[150] == Approx(ref0[3]).margin(m)); + REQUIRE(output[200] == Approx(ref0[4]).margin(m)); + REQUIRE(output[250] == Approx(ref0[5]).margin(m)); + REQUIRE(output[300] == Approx(ref0[6]).margin(m)); + break; + case 1: + REQUIRE(output[ 0] == Approx(ref1[0]).margin(m)); + REQUIRE(output[ 50] == Approx(ref1[1]).margin(m)); + REQUIRE(output[100] == Approx(ref1[2]).margin(m)); + REQUIRE(output[150] == Approx(ref1[3]).margin(m)); + REQUIRE(output[200] == Approx(ref1[4]).margin(m)); + break; + case 2: + REQUIRE(output[ 0] == Approx(ref2[0]).margin(m)); + REQUIRE(output[ 50] == Approx(ref2[1]).margin(m)); + REQUIRE(output[100] == Approx(ref2[2]).margin(m)); + break; + } + } +} From d8eaf4a9876bd9db70707b5a144f9c7cb9eeb443 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 29 Sep 2020 01:26:04 +0200 Subject: [PATCH 344/445] Fix pitch_veltrack --- src/sfizz/Region.cpp | 2 +- tests/RegionT.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index c599bc66..1cc907d2 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1707,7 +1707,7 @@ float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const auto pitchVariationInCents = pitchKeytrack * (noteNumber - pitchKeycenter); // note difference with pitch center pitchVariationInCents += tune; // sample tuning pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose - pitchVariationInCents += static_cast(velocity) * pitchVeltrack; // track velocity + pitchVariationInCents += velocity * pitchVeltrack; // track velocity pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes return centsFactor(pitchVariationInCents); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index cc2714a0..77aff25a 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1845,3 +1845,17 @@ TEST_CASE("[Region] Offsets with CCs") midiState.ccEvent(0, 4, 0); REQUIRE( region.getOffset() == 10 ); } + +TEST_CASE("[Region] Pitch variation with veltrack") +{ + MidiState midiState; + Region region { 0, midiState }; + + REQUIRE(region.getBasePitchVariation(60.0, 0_norm) == 1.0); + REQUIRE(region.getBasePitchVariation(60.0, 64_norm) == 1.0); + REQUIRE(region.getBasePitchVariation(60.0, 127_norm) == 1.0); + region.parseOpcode({ "pitch_veltrack", "1200" }); + REQUIRE(region.getBasePitchVariation(60.0, 0_norm) == 1.0); + REQUIRE(region.getBasePitchVariation(60.0, 64_norm) == Approx(centsFactor(600.0)).margin(0.01f)); + REQUIRE(region.getBasePitchVariation(60.0, 127_norm) == Approx(centsFactor(1200.0)).margin(0.01f)); +} From 4d20f7722a3f6d082120b12e14e6f1a9074faacc Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 29 Sep 2020 02:40:38 +0200 Subject: [PATCH 345/445] Add math helper: fast fmod --- src/sfizz/MathHelpers.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index f0dbebc4..d5b0cd3a 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -139,6 +139,20 @@ constexpr T clamp(T v, T lo, T hi) return max(min(v, hi), lo); } +/** + * @brief Compute the floating-point remainder (fmod) + * + * @tparam T + * @param x + * @param m + * @return T + */ +template +inline constexpr T fastFmod(T x, T m) +{ + return x - m * static_cast(x / m); +} + template inline CXX14_CONSTEXPR void incrementAll(T& only) { From d6e8d547e4c1dda80b2d2f595581238341ea61bf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 29 Sep 2020 03:20:02 +0200 Subject: [PATCH 346/445] Fix the case when multiple loops occur in one buffer --- src/sfizz/Voice.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a60b1af7..1c36df54 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -549,12 +549,12 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) { const auto loopEnd = static_cast(region->loopEnd(currentPromise->oversamplingFactor)); - const auto offset = loopEnd - static_cast(region->loopStart(currentPromise->oversamplingFactor)) + 1; - for (auto* index = indices->begin(); index < indices->end(); ++index) { - if (*index > loopEnd) { - const auto remainingElements = static_cast(std::distance(index, indices->end())); - subtract1(offset, { index, remainingElements }); - } + const auto loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); + const auto loopSize = loopEnd + 1 - loopStart; + for (auto* it = indices->begin(), *end = indices->end(); it < end; ++it) { + auto index = *it; + *it = (index < loopEnd + 1) ? index : + (loopStart + (index - loopStart) % loopSize); } } else { const auto sampleEnd = min( From 01a26787e3c2cb42db16fe7458dab5598c8c7215 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 01:41:22 +0200 Subject: [PATCH 347/445] Fix return missing in move operator= --- src/sfizz/BufferPool.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/BufferPool.h b/src/sfizz/BufferPool.h index 2d057edf..b9971fcf 100644 --- a/src/sfizz/BufferPool.h +++ b/src/sfizz/BufferPool.h @@ -36,6 +36,7 @@ public: this->value = other.value; this->available = other.available; other.available = nullptr; + return *this; } SpanHolder(T&& value, int* available) : value(std::forward(value)) From 824689e38a6f76e4d67b149681cdfda34141700a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 01:41:48 +0200 Subject: [PATCH 348/445] Fix a misuse of const qualification in AudioSpan --- src/sfizz/AudioSpan.h | 12 ++++++------ src/sfizz/Voice.cpp | 2 +- src/sfizz/Voice.h | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sfizz/AudioSpan.h b/src/sfizz/AudioSpan.h index 0791eafa..fa879312 100644 --- a/src/sfizz/AudioSpan.h +++ b/src/sfizz/AudioSpan.h @@ -200,7 +200,7 @@ public: * @param channelIndex the channel * @return Type* the raw pointer to the channel */ - Type* getChannel(size_t channelIndex) + Type* getChannel(size_t channelIndex) const { ASSERT(channelIndex < numChannels); if (channelIndex < numChannels) @@ -231,7 +231,7 @@ public: * @param channelIndex the channel * @return absl::Span */ - absl::Span getSpan(size_t channelIndex) + absl::Span getSpan(size_t channelIndex) const { ASSERT(channelIndex < numChannels); if (channelIndex < numChannels) @@ -402,7 +402,7 @@ public: * * @param length the number of elements to take on each channel */ - AudioSpan first(size_type length) + AudioSpan first(size_type length) const { ASSERT(length <= numFrames); return { spans, numChannels, 0, length }; @@ -413,7 +413,7 @@ public: * * @param length the number of elements to take on each channel */ - AudioSpan last(size_type length) + AudioSpan last(size_type length) const { ASSERT(length <= numFrames); return { spans, numChannels, numFrames - length, length }; @@ -427,7 +427,7 @@ public: * * @param length the number of elements to take on each channel */ - AudioSpan subspan(size_type offset, size_type length) + AudioSpan subspan(size_type offset, size_type length) const { ASSERT(length + offset <= numFrames); return { spans, numChannels, offset, length }; @@ -440,7 +440,7 @@ public: * * @param length the number of elements to take on each channel */ - AudioSpan subspan(size_type offset) + AudioSpan subspan(size_type offset) const { ASSERT(offset <= numFrames); return { spans, numChannels, offset, numFrames - offset }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 1c36df54..4a0fc5b0 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -615,7 +615,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept template void sfz::Voice::fillInterpolated( - const sfz::AudioSpan& source, sfz::AudioSpan& dest, + const sfz::AudioSpan& source, const sfz::AudioSpan& dest, absl::Span indices, absl::Span coeffs) { auto ind = indices.data(); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 419c6584..9d5bfceb 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -376,7 +376,7 @@ private: */ template static void fillInterpolated( - const AudioSpan& source, AudioSpan& dest, + const AudioSpan& source, const AudioSpan& dest, absl::Span indices, absl::Span coeffs); /** From f6d05509cbf523e02dad0775a6bbe43b392a741f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 29 Sep 2020 23:52:10 +0200 Subject: [PATCH 349/445] Add opcode: loop_crossfade --- src/sfizz/Defaults.h | 2 ++ src/sfizz/Region.cpp | 3 +++ src/sfizz/Region.h | 1 + tests/RegionT.cpp | 8 ++++++++ 4 files changed, 14 insertions(+) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 540b6944..bf4c4ae6 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -54,6 +54,8 @@ namespace Default constexpr Range sampleCountRange { 0, std::numeric_limits::max() }; constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop }; constexpr Range loopRange { 0, std::numeric_limits::max() }; + constexpr float loopCrossfade { 1e-3 }; + constexpr Range loopCrossfadeRange { loopCrossfade, 1.0 }; // common defaults constexpr Range midi7Range { 0, 127 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 2e9a1de5..8c85cca1 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -138,6 +138,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("loop_start"): // also loopstart setRangeStartFromOpcode(opcode, loopRange, Default::loopRange); break; + case hash("loop_crossfade"): + setValueFromOpcode(opcode, loopCrossfade, Default::loopCrossfadeRange); + break; // Wavetable oscillator case hash("oscillator_phase"): diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 76840c53..a8144d52 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -319,6 +319,7 @@ struct Region { absl::optional sampleCount {}; // count absl::optional loopMode {}; // loopmode Range loopRange { Default::loopRange }; //loopstart and loopend + float loopCrossfade { Default::loopCrossfade }; // loop_crossfade // Wavetable oscillator float oscillatorPhase { Default::oscillatorPhase }; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 77aff25a..550da46f 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -174,6 +174,14 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.loopRange == Range(0, 4294967295)); } + SECTION("loop_crossfade") + { + region.parseOpcode({ "loop_crossfade", "0.5" }); + REQUIRE(region.loopCrossfade == Approx(0.5f)); + region.parseOpcode({ "loop_crossfade", "0" }); + REQUIRE(region.loopCrossfade > 0); + } + SECTION("group") { REQUIRE(region.group == 0); From 8a2e17ec921a4de64faee390599d45e535e1df30 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 01:27:32 +0200 Subject: [PATCH 350/445] Implement loop xfade --- src/sfizz/Config.h | 2 +- src/sfizz/Voice.cpp | 299 +++++++++++++++++++++++++++++++++++++------- src/sfizz/Voice.h | 21 +++- 3 files changed, 277 insertions(+), 45 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 442a165c..3c724dad 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -31,7 +31,7 @@ namespace config { constexpr int maxBlockSize { 8192 }; constexpr int bufferPoolSize { 6 }; constexpr int stereoBufferPoolSize { 4 }; - constexpr int indexBufferPoolSize { 2 }; + constexpr int indexBufferPoolSize { 4 }; constexpr int preloadSize { 8192 }; constexpr int loggerQueueSize { 256 }; constexpr int voiceLoggerQueueSize { 256 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4a0fc5b0..a370044f 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -533,35 +533,133 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept auto source = currentPromise->getData(); - auto jumps = resources.bufferPool.getBuffer(numSamples); + // calculate interpolation data + // indices: integral position in the source audio + // coeffs: fractional position normalized 0-1 auto coeffs = resources.bufferPool.getBuffer(numSamples); auto indices = resources.bufferPool.getIndexBuffer(numSamples); - if (!jumps || !indices || !coeffs) + if (!indices || !coeffs) return; + { + auto jumps = resources.bufferPool.getBuffer(numSamples); + if (!jumps) + return; - fill(*jumps, pitchRatio * speedRatio); - pitchEnvelope(*jumps); + fill(*jumps, pitchRatio * speedRatio); + pitchEnvelope(*jumps); - jumps->front() += floatPositionOffset; - cumsum(*jumps, *jumps); - sfzInterpolationCast(*jumps, *indices, *coeffs); - add1(sourcePosition, *indices); + jumps->front() += floatPositionOffset; + cumsum(*jumps, *jumps); + sfzInterpolationCast(*jumps, *indices, *coeffs); + add1(sourcePosition, *indices); + } - if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) { - const auto loopEnd = static_cast(region->loopEnd(currentPromise->oversamplingFactor)); - const auto loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); - const auto loopSize = loopEnd + 1 - loopStart; - for (auto* it = indices->begin(), *end = indices->end(); it < end; ++it) { - auto index = *it; - *it = (index < loopEnd + 1) ? index : - (loopStart + (index - loopStart) % loopSize); + // calculate loop characteristics + bool isLooping = false; + int loopStart = 0; + int loopEnd = 0; + int loopSize = 0; + int loopXfadeSize = 0; + int loopXfOutStart = 0; + int loopXfInStart = 0; // Note: beware in case of negative index + SpanHolder> xfadeTemp[2]; + SpanHolder> xfadeIndexTemp[1]; + if (region->shouldLoop()) { + loopEnd = region->loopEnd(currentPromise->oversamplingFactor); + isLooping = static_cast(loopEnd) < source.getNumFrames(); + } + if (isLooping) { + loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); + loopSize = loopEnd + 1 - loopStart; + loopXfadeSize = static_cast(region->loopCrossfade * sampleRate + 0.5); + loopXfOutStart = loopEnd + 1 - loopXfadeSize; + loopXfInStart = loopStart - loopXfadeSize; + for (auto& buf : xfadeTemp) { + buf = resources.bufferPool.getBuffer(numSamples); + if (!buf) + return; } - } else { + for (auto& buf : xfadeIndexTemp) { + buf = resources.bufferPool.getIndexBuffer(numSamples); + if (!buf) + return; + } + } + + /* + loop start loop end + v | + /|---------------|\ | + / | | \ | + / | | \ v + /------|---------------|------\ + ^ ^ + xfin start xfout start + <------> <------> + xfade size xfade size + */ + + // loop crossfade partitioning + absl::Span partitionStarts; + absl::Span partitionTypes; + unsigned numPartitions = 0; + enum PartitionType { kPartitionNormal, kPartitionLoopXfade }; + + SpanHolder> partitionBuffers[2]; + if (!isLooping) { + static const int starts[1] = { 0 }; + static const int types[1] = { kPartitionNormal }; + partitionStarts = absl::MakeSpan(const_cast(starts), 1); + partitionTypes = absl::MakeSpan(const_cast(types), 1); + numPartitions = 1; + } + else { + for (auto& buf : partitionBuffers) { + buf = resources.bufferPool.getIndexBuffer(numSamples); + if (!buf) + return; + } + partitionStarts = *partitionBuffers[0]; + partitionTypes = *partitionBuffers[1]; + // Note: partitions will be alternance of Normal/Xfade + // computed along with index processing below + } + + // index preprocessing for loops + if (isLooping) { + int oldIndex {}; + int oldPartitionType {}; + for (unsigned i = 0; i < numSamples; ++i) { + int index = (*indices)[i]; + + // wrap indices post loop-entry around the loop segment + int wrappedIndex = (index <= loopEnd) ? index : + (loopStart + (index - loopStart) % loopSize); + (*indices)[i] = wrappedIndex; + + // identify the partition this index is in + bool xfading = wrappedIndex >= loopStart && wrappedIndex >= loopXfOutStart; + int partitionType = xfading ? kPartitionLoopXfade : kPartitionNormal; + // if looping or entering a different type, start a new partition + bool start = i == 0 || wrappedIndex < oldIndex || partitionType != oldPartitionType; + if (start) { + partitionStarts[numPartitions] = i; + partitionTypes[numPartitions] = partitionType; + ++numPartitions; + } + + oldIndex = wrappedIndex; + oldPartitionType = partitionType; + } + } + // index preprocessing for one-shots + else { + // cut short the voice at the instant of reaching end of sample const auto sampleEnd = min( static_cast(region->trueSampleEnd(currentPromise->oversamplingFactor)), static_cast(source.getNumFrames()) ) - 1; - for (unsigned i = 0; i < indices->size(); ++i) { + for (unsigned i = 0; i < numSamples; ++i) { if ((*indices)[i] >= sampleEnd) { #ifndef NDEBUG // Check for underflow @@ -581,25 +679,93 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } } + // interpolation processing const int quality = getCurrentSampleQuality(); - switch (quality) { - default: - if (quality > 2) - goto high; // TODO sinc, not implemented - // fall through - case 1: - fillInterpolated(source, buffer, *indices, *coeffs); - break; - case 2: high: -#if 1 - // B-spline response has faster decay of aliasing, but not zero-crossings at integer positions - fillInterpolated(source, buffer, *indices, *coeffs); -#else - // Hermite polynomial - fillInterpolated(source, buffer, *indices, *coeffs); -#endif - break; + for (unsigned ptNo = 0; ptNo < numPartitions; ++ptNo) { + // current partition + const int ptType = partitionTypes[ptNo]; + const unsigned ptStart = partitionStarts[ptNo]; + const unsigned ptNextStart = (ptNo + 1 < numPartitions) ? partitionStarts[ptNo + 1] : numSamples; + const unsigned ptSize = ptNextStart - ptStart; + + // partition spans + AudioSpan ptBuffer = buffer.subspan(ptStart, ptSize); + absl::Span ptIndices = indices->subspan(ptStart, ptSize); + absl::Span ptCoeffs = coeffs->subspan(ptStart, ptSize); + + fillInterpolatedWithQuality( + source, ptBuffer, ptIndices, ptCoeffs, {}, quality); + + if (ptType == kPartitionLoopXfade) { + absl::Span xfCoeff = xfadeTemp[0]->first(ptSize); + + // compute crossfade coeffs + for (unsigned i = 0; i < ptSize; ++i) { + float pos = ptIndices[i] + ptCoeffs[i]; + xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; + } + + //----------------------------------------------------------------// + // Crossfade Out + // -> fade out signal nearing the loop end + { + // compute crossfade coeffs + for (unsigned i = 0; i < ptSize; ++i) { + float pos = ptIndices[i] + ptCoeffs[i]; + xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; + } + // compute out curve + const Curve& xfOut = resources.curves.getCurve(6); + absl::Span xfCurve = xfadeTemp[1]->first(ptSize); + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + // apply out curve + if (0) + ptBuffer.applyGain(xfCurve); + else { + // scalar fallback: buffer and curve not aligned + size_t numChannels = ptBuffer.getNumChannels(); + for (size_t c = 0; c < numChannels; ++c) { + absl::Span channel = ptBuffer.getSpan(c); + for (unsigned i = 0; i < ptSize; ++i) + channel[i] *= xfCurve[i]; + } + } + } + //----------------------------------------------------------------// + // Crossfade In + // -> fade in signal preceding the loop start + { + // compute indices of the crossfade input segment + absl::Span xfInIndices = xfadeIndexTemp[0]->first(ptSize); + absl::c_copy(ptIndices, xfInIndices.begin()); + subtract1(loopXfOutStart - loopXfInStart, xfInIndices); + + // disregard the segment whose indices have been pushed + // into the negatives, take these virtually as zeroes. + unsigned applyOffset = 0; + while (applyOffset < ptSize && xfInIndices[applyOffset] < 0) + ++applyOffset; + unsigned applySize = ptSize - applyOffset; + + // offset the indices + xfInIndices = xfInIndices.subspan(applyOffset); + // offset the coeffs + absl::Span xfInCoeff = xfCoeff.subspan(applyOffset); + // offset the output buffer + AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); + + // compute in curve + const Curve& xfIn = resources.curves.getCurve(5); + absl::Span xfCurve = xfadeTemp[1]->first(applySize); + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + // apply in curve + fillInterpolatedWithQuality( + source, xfInBuffer, xfInIndices, *coeffs, xfCurve, quality); + } + } } sourcePosition = indices->back(); @@ -613,31 +779,80 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept #endif } -template +template void sfz::Voice::fillInterpolated( const sfz::AudioSpan& source, const sfz::AudioSpan& dest, - absl::Span indices, absl::Span coeffs) + absl::Span indices, absl::Span coeffs, + absl::Span addingGains) { - auto ind = indices.data(); - auto coeff = coeffs.data(); + auto* ind = indices.data(); + auto* coeff = coeffs.data(); + auto* addingGain = addingGains.data(); auto leftSource = source.getConstSpan(0); auto left = dest.getChannel(0); if (source.getNumChannels() == 1) { while (ind < indices.end()) { - *left = sfz::interpolate(&leftSource[*ind], *coeff); + auto output = sfz::interpolate(&leftSource[*ind], *coeff); + IF_CONSTEXPR(Adding) { + float g = *addingGain++; + *left += g * output; + } + else + *left = output; incrementAll(ind, left, coeff); } } else { auto right = dest.getChannel(1); auto rightSource = source.getConstSpan(1); while (ind < indices.end()) { - *left = sfz::interpolate(&leftSource[*ind], *coeff); - *right = sfz::interpolate(&rightSource[*ind], *coeff); + auto leftOutput = sfz::interpolate(&leftSource[*ind], *coeff); + auto rightOutput = sfz::interpolate(&rightSource[*ind], *coeff); + IF_CONSTEXPR(Adding) { + float g = *addingGain++; + *left += g * leftOutput; + *right += g * rightOutput; + } + else { + *left = leftOutput; + *right = rightOutput; + } incrementAll(ind, left, right, coeff); } } } +template +void sfz::Voice::fillInterpolatedWithQuality( + const sfz::AudioSpan& source, const sfz::AudioSpan& dest, + absl::Span indices, absl::Span coeffs, + absl::Span addingGains, int quality) +{ + switch (quality) { + default: + if (quality > 2) + goto high; // TODO sinc, not implemented + // fall through + case 1: + { + constexpr auto itp = kInterpolatorLinear; + fillInterpolated(source, dest, indices, coeffs, addingGains); + } + break; + case 2: high: + { +#if 1 + // B-spline response has faster decay of aliasing, but not zero-crossings at integer positions + constexpr auto itp = kInterpolatorBspline3; +#else + // Hermite polynomial + constexpr auto itp = kInterpolatorHermite3; +#endif + fillInterpolated(source, dest, indices, coeffs, addingGains); + } + break; + } +} + void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept { const auto leftSpan = buffer.getSpan(0); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 9d5bfceb..35f91efc 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -374,10 +374,27 @@ private: * @param indices the integral parts of the source positions * @param coeffs the fractional parts of the source positions */ - template + template static void fillInterpolated( const AudioSpan& source, const AudioSpan& dest, - absl::Span indices, absl::Span coeffs); + absl::Span indices, absl::Span coeffs, + absl::Span addingGains); + + /** + * @brief Fill a destination with an interpolated source, selecting + * interpolation type dynamically by quality level. + * + * @param source the source sample + * @param dest the destination buffer + * @param indices the integral parts of the source positions + * @param coeffs the fractional parts of the source positions + * @param quality the quality level 1-10 + */ + template + static void fillInterpolatedWithQuality( + const AudioSpan& source, const AudioSpan& dest, + absl::Span indices, absl::Span coeffs, + absl::Span addingGains, int quality); /** * @brief Compute the amplitude envelope, applied as a gain to a mono From 7a5fd53001ac916a9a2f4f900c073ef62410b5c7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 18:19:31 +0200 Subject: [PATCH 351/445] Let's make clang-tidy happy --- src/sfizz/Voice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a370044f..0b1ee1aa 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -571,7 +571,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept if (isLooping) { loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); loopSize = loopEnd + 1 - loopStart; - loopXfadeSize = static_cast(region->loopCrossfade * sampleRate + 0.5); + loopXfadeSize = static_cast(lroundPositive(region->loopCrossfade * sampleRate)); loopXfOutStart = loopEnd + 1 - loopXfadeSize; loopXfInStart = loopStart - loopXfadeSize; for (auto& buf : xfadeTemp) { From fda4b5931c364655332a08fedb24adb6e4eba6d8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 19:35:43 +0200 Subject: [PATCH 352/445] Eliminate a code repetition --- src/sfizz/Voice.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 0b1ee1aa..dbeb6405 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -710,11 +710,6 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // Crossfade Out // -> fade out signal nearing the loop end { - // compute crossfade coeffs - for (unsigned i = 0; i < ptSize; ++i) { - float pos = ptIndices[i] + ptCoeffs[i]; - xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; - } // compute out curve const Curve& xfOut = resources.curves.getCurve(6); absl::Span xfCurve = xfadeTemp[1]->first(ptSize); From 453d489d4ae458175e599d77f2f776712bd3498e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 20:29:50 +0200 Subject: [PATCH 353/445] Use linear xfade curves --- src/sfizz/Voice.cpp | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index dbeb6405..e0f298bd 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -625,6 +625,9 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // computed along with index processing below } + // loop crossfade settings + constexpr bool loopXfadeUseCurves = false; // 0: linear, 1: use curves 5 & 6 + // index preprocessing for loops if (isLooping) { int oldIndex {}; @@ -711,10 +714,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // -> fade out signal nearing the loop end { // compute out curve - const Curve& xfOut = resources.curves.getCurve(6); absl::Span xfCurve = xfadeTemp[1]->first(ptSize); - for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + IF_CONSTEXPR (loopXfadeUseCurves) { + const Curve& xfOut = resources.curves.getCurve(6); + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + } + else { + // TODO(jpc) vectorize this + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = clamp(1.0f - xfCoeff[i], 0.0f, 1.0f); + } // apply out curve if (0) ptBuffer.applyGain(xfCurve); @@ -752,10 +762,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); // compute in curve - const Curve& xfIn = resources.curves.getCurve(5); absl::Span xfCurve = xfadeTemp[1]->first(applySize); - for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + IF_CONSTEXPR (loopXfadeUseCurves) { + const Curve& xfIn = resources.curves.getCurve(5); + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + } + else { + // TODO(jpc) vectorize this + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = clamp(xfInCoeff[i], 0.0f, 1.0f); + } // apply in curve fillInterpolatedWithQuality( source, xfInBuffer, xfInIndices, *coeffs, xfCurve, quality); From 893d0361c2015ebff83969a401d5fc3f559eecdf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 1 Oct 2020 13:43:22 +0200 Subject: [PATCH 354/445] Crossfade with S-shape curve --- src/sfizz/Voice.cpp | 39 ++++++++++++++++++++++++++++++++++----- src/sfizz/Voice.h | 5 +++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index e0f298bd..574fd2a9 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -34,6 +34,9 @@ sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources) gainSmoother.setSmoothing(config::gainSmoothing, sampleRate); xfadeSmoother.setSmoothing(config::xfadeSmoothing, sampleRate); + + // prepare curves + getSCurve(); } sfz::Voice::~Voice() @@ -626,7 +629,9 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } // loop crossfade settings - constexpr bool loopXfadeUseCurves = false; // 0: linear, 1: use curves 5 & 6 + constexpr int loopXfadeUseCurves = 2; // 0: linear + // 1: use curves 5 & 6 + // 2: use S-shaped curve // index preprocessing for loops if (isLooping) { @@ -715,12 +720,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept { // compute out curve absl::Span xfCurve = xfadeTemp[1]->first(ptSize); - IF_CONSTEXPR (loopXfadeUseCurves) { + IF_CONSTEXPR (loopXfadeUseCurves == 2) { + const Curve& xfIn = getSCurve(); + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = xfIn.evalNormalized(1.0f - xfCoeff[i]); + } + else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfOut = resources.curves.getCurve(6); for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); } - else { + else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = clamp(1.0f - xfCoeff[i], 0.0f, 1.0f); @@ -763,12 +773,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute in curve absl::Span xfCurve = xfadeTemp[1]->first(applySize); - IF_CONSTEXPR (loopXfadeUseCurves) { + IF_CONSTEXPR (loopXfadeUseCurves == 2) { + const Curve& xfIn = getSCurve(); + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + } + else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfIn = resources.curves.getCurve(5); for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); } - else { + else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = clamp(xfInCoeff[i], 0.0f, 1.0f); @@ -865,6 +880,20 @@ void sfz::Voice::fillInterpolatedWithQuality( } } +const sfz::Curve& sfz::Voice::getSCurve() +{ + static const Curve curve = []() -> Curve { + constexpr unsigned N = Curve::NumValues; + float values[N]; + for (unsigned i = 0; i < N; ++i) { + double x = i / static_cast(N - 1); + values[i] = (1.0 - std::cos(M_PI * x)) * 0.5; + } + return Curve::buildFromPoints(values); + }(); + return curve; +} + void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept { const auto leftSpan = buffer.getSpan(0); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 35f91efc..95dbd9ec 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -396,6 +396,11 @@ private: absl::Span indices, absl::Span coeffs, absl::Span addingGains, int quality); + /** + * @brief Get a S-shaped curve that is applicable to loop crossfading. + */ + static const Curve& getSCurve(); + /** * @brief Compute the amplitude envelope, applied as a gain to a mono * or stereo buffer From ab35581786e7f563fb510704eb677d2cfa0e24c5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 1 Oct 2020 14:03:10 +0200 Subject: [PATCH 355/445] Xfade related to file sample rate, with oversampling considered --- src/sfizz/Voice.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 574fd2a9..8f655f77 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -574,7 +574,8 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept if (isLooping) { loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); loopSize = loopEnd + 1 - loopStart; - loopXfadeSize = static_cast(lroundPositive(region->loopCrossfade * sampleRate)); + loopXfadeSize = static_cast( + lroundPositive(region->loopCrossfade * static_cast(currentPromise->oversamplingFactor) * currentPromise->sampleRate)); loopXfOutStart = loopEnd + 1 - loopXfadeSize; loopXfInStart = loopStart - loopXfadeSize; for (auto& buf : xfadeTemp) { From 0b502b44f13e44dd22b2a3f6675d3aca2a4a59a5 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 3 Oct 2020 10:01:08 +0200 Subject: [PATCH 356/445] Don't assume a single for region sets --- src/sfizz/Opcode.h | 2 +- src/sfizz/RegionSet.h | 16 ++++++++++++++++ src/sfizz/Synth.cpp | 38 +++++++++++++++----------------------- src/sfizz/Synth.h | 3 +-- tests/PolyphonyT.cpp | 12 ++++++------ 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index a3132499..a6c3a1d6 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -43,7 +43,7 @@ enum OpcodeCategory { */ enum OpcodeScope { //! unknown scope or other - kOpcodeScopeGeneric, + kOpcodeScopeGeneric = 0, //! global scope kOpcodeScopeGlobal, //! control scope diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index bbfadce0..4424a030 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -8,6 +8,7 @@ #include "Region.h" #include "Voice.h" +#include "Opcode.h" #include "SwapAndPop.h" #include @@ -16,6 +17,13 @@ namespace sfz class RegionSet { public: + RegionSet() = delete; + RegionSet(RegionSet* parentSet, OpcodeScope level) + : parent(parentSet), level(level) + { + if (parentSet != nullptr) + parentSet->addSubset(this); + } /** * @brief Set the polyphony limit for the set * @@ -73,6 +81,13 @@ public: * @return RegionSet* */ RegionSet* getParent() const noexcept { return parent; } + + /** + * @brief Get the set level + * + * @return OpcodeScope + */ + OpcodeScope getLevel() const noexcept { return level; } /** * @brief Set the parent set * @@ -109,6 +124,7 @@ public: const std::vector& getSubsets() const noexcept { return subsets; } private: RegionSet* parent { nullptr }; + OpcodeScope level { kOpcodeScopeGeneric }; std::vector regions; std::vector subsets; std::vector voices; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b191d841..8a093612 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -77,20 +77,19 @@ void sfz::Synth::onVoiceStateChanged(NumericId id, Voice::State state) void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector& members) { - const auto newRegionSet = [&](RegionSet* parentSet) { - ASSERT(parentSet != nullptr); - sets.emplace_back(new RegionSet); - auto newSet = sets.back().get(); - parentSet->addSubset(newSet); - newSet->setParent(parentSet); - currentSet = newSet; + const auto newRegionSet = [&](OpcodeScope level) { + auto parent = currentSet; + while (parent && parent->getLevel() >= level) + parent = parent->getParent(); + + sets.emplace_back(new RegionSet(parent, level)); + currentSet = sets.back().get(); }; switch (hash(header)) { case hash("global"): globalOpcodes = members; - currentSet = sets.front().get(); - lastHeader = OpcodeScope::kOpcodeScopeGlobal; + newRegionSet(OpcodeScope::kOpcodeScopeGlobal); groupOpcodes.clear(); masterOpcodes.clear(); handleGlobalOpcodes(members); @@ -101,19 +100,14 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); - else - newRegionSet(currentSet); - lastHeader = OpcodeScope::kOpcodeScopeGroup; + newRegionSet(OpcodeScope::kOpcodeScopeGroup); handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -145,8 +139,6 @@ void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& mes void sfz::Synth::buildRegion(const std::vector& regionOpcodes) { - ASSERT(currentSet != nullptr); - int regionNumber = static_cast(regions.size()); auto lastRegion = absl::make_unique(regionNumber, resources.midiState, defaultPath); @@ -184,8 +176,10 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) if (lastRegion->group != Default::group && lastRegion->polyphony != config::maxVoices) setGroupPolyphony(lastRegion->group, lastRegion->polyphony); - lastRegion->parent = currentSet; - currentSet->addRegion(lastRegion.get()); + if (currentSet != nullptr) { + lastRegion->parent = currentSet; + currentSet->addRegion(lastRegion.get()); + } // Adapt the size of the delayed releases to avoid allocating later on lastRegion->delayedReleases.reserve(lastRegion->keyRange.length()); @@ -204,10 +198,8 @@ void sfz::Synth::clear() for (auto& list : ccActivationLists) list.clear(); - lastHeader = OpcodeScope::kOpcodeScopeGlobal; + currentSet = nullptr; sets.clear(); - sets.emplace_back(new RegionSet); - currentSet = sets.front().get(); regions.clear(); effectBuses.clear(); effectBuses.emplace_back(new EffectBus); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 880726cf..c0d32c2e 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -817,8 +817,7 @@ private: std::vector voices; // These are more general "groups" than sfz and encapsulates the full hierarchy - RegionSet* currentSet; - OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal }; + RegionSet* currentSet { nullptr }; std::vector sets; // These are the `group=` groups where you can off voices diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index b7de5c7a..c1d891d4 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -33,13 +33,13 @@ TEST_CASE("[Polyphony] Polyphony in hierarchy") key=64 sample=*sine )"); REQUIRE( synth.getRegionView(0)->polyphony == 2 ); - REQUIRE( synth.getRegionSetView(1)->getPolyphonyLimit() == 2 ); + REQUIRE( synth.getRegionSetView(0)->getPolyphonyLimit() == 2 ); REQUIRE( synth.getRegionView(1)->polyphony == 2 ); - REQUIRE( synth.getRegionSetView(2)->getPolyphonyLimit() == 3 ); - REQUIRE( synth.getRegionSetView(2)->getRegions()[0]->polyphony == 3 ); - REQUIRE( synth.getRegionSetView(3)->getPolyphonyLimit() == 4 ); - REQUIRE( synth.getRegionSetView(3)->getRegions()[0]->polyphony == 5 ); - REQUIRE( synth.getRegionSetView(3)->getRegions()[1]->polyphony == 4 ); + REQUIRE( synth.getRegionSetView(1)->getPolyphonyLimit() == 3 ); + REQUIRE( synth.getRegionSetView(1)->getRegions()[0]->polyphony == 3 ); + REQUIRE( synth.getRegionSetView(2)->getPolyphonyLimit() == 4 ); + REQUIRE( synth.getRegionSetView(2)->getRegions()[0]->polyphony == 5 ); + REQUIRE( synth.getRegionSetView(2)->getRegions()[1]->polyphony == 4 ); } TEST_CASE("[Polyphony] Polyphony groups") From f27c1e4c31f6c9645ec42713b5105ccb735d7d7d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 4 Oct 2020 16:52:45 +0200 Subject: [PATCH 357/445] Put a high amplitude range --- src/sfizz/Defaults.h | 2 +- tests/RegionT.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 540b6944..603e36b7 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -122,7 +122,7 @@ namespace Default constexpr Range volumeRange { -144.0, 48.0 }; constexpr Range volumeCCRange { -144.0, 48.0 }; constexpr float amplitude { 100.0 }; - constexpr Range amplitudeRange { 0.0, 100.0 }; + constexpr Range amplitudeRange { 0.0, 1e8 }; constexpr float pan { 0.0 }; constexpr Range panRange { -100.0, 100.0 }; constexpr Range panCCRange { -200.0, 200.0 }; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 77aff25a..b07f4816 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -925,7 +925,7 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ a.first, "-40" }); REQUIRE(*a.second == 0_a); region.parseOpcode({ a.first, "140" }); - REQUIRE(*a.second == 1.0_a); + REQUIRE(*a.second == 1.4_a); } } @@ -1640,7 +1640,7 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "amplitude", "-40" }); REQUIRE(region.amplitude == 0_a); region.parseOpcode({ "amplitude", "140" }); - REQUIRE(region.amplitude == 1.0_a); + REQUIRE(region.amplitude == 1.4_a); } SECTION("amplitude_cc") @@ -1667,7 +1667,7 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "amplitude_stepcc120", "24" }); REQUIRE(view.at(120).step == 24.0_a); region.parseOpcode({ "amplitude_stepcc120", "15482" }); - REQUIRE(view.at(120).step == 100.0_a); + REQUIRE(view.at(120).step == 15482.0_a); region.parseOpcode({ "amplitude_stepcc120", "-2" }); REQUIRE(view.at(120).step == 0.0f); } From 6156022628f484642d4276997a6879c86ce677d6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 4 Oct 2020 17:32:44 +0200 Subject: [PATCH 358/445] this use of getSourcePosition is wrong (sourcePosition is not increased if e.g. the voice is a generator for example) To avoid this I removed the API which has really no purpose --- src/sfizz/Synth.cpp | 2 +- src/sfizz/Voice.cpp | 5 ----- src/sfizz/Voice.h | 6 ------ 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b191d841..2d8b01d6 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1018,7 +1018,7 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const Trigg } break; case SfzSelfMask::dontMask: - if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition()) + if (!selfMaskCandidate || selfMaskCandidate->getAge() < voice->getAge()) selfMaskCandidate = voice; break; } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4a0fc5b0..4afc7bc2 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -856,11 +856,6 @@ bool sfz::Voice::releasedOrFree() const noexcept return state != State::playing || egAmplitude.isReleased(); } -uint32_t sfz::Voice::getSourcePosition() const noexcept -{ - return sourcePosition; -} - void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters) { if (numFilters == filters.size()) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 9d5bfceb..70e5fb74 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -248,12 +248,6 @@ public: * @return float */ float getAveragePower() const noexcept; - /** - * @brief Get the position of the voice in the source, in samples - * - * @return uint32_t - */ - uint32_t getSourcePosition() const noexcept; /** * Returns the region that is currently playing. May be null if the voice is not active! * From 0046bb69e64817ef9be9c36f4f2fb4271208252d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 4 Oct 2020 17:32:58 +0200 Subject: [PATCH 359/445] Set the proper oversampling factor for the initial offset --- src/sfizz/Voice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4afc7bc2..27323c6d 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -97,6 +97,7 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event return; } speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); + sourcePosition = region->getOffset(currentPromise->oversamplingFactor); } // do Scala retuning and reconvert the frequency into a 12TET key number @@ -123,7 +124,6 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event equalizers[i].setup(*region, i, triggerEvent.value); } - sourcePosition = region->getOffset(); triggerDelay = delay; initialDelay = delay + static_cast(region->getDelay() * sampleRate); baseFrequency = resources.tuning.getFrequencyOfKey(triggerEvent.number); From ab3715f7a91769c1db43dc7bd19fc17b3c288e2c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 4 Oct 2020 23:00:21 +0200 Subject: [PATCH 360/445] Read 0 or 1 values as valid booleans --- src/sfizz/Opcode.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Opcode.cpp b/src/sfizz/Opcode.cpp index 1ffafdbf..673501d1 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -211,9 +211,11 @@ absl::optional readOpcode(absl::string_view value, const Range readBooleanFromOpcode(const Opcode& opcode) { switch (hash(opcode.value)) { - case hash("off"): + case hash("off"): // fallthrough + case hash("0"): return false; - case hash("on"): + case hash("on"): // fallthrough + case hash("1"): return true; default: return {}; From 5b4e673fe7437aa8ebe72264792ca8bb78023e3c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 4 Oct 2020 23:00:52 +0200 Subject: [PATCH 361/445] Use the new boolean helper and test the ampeg connection --- src/sfizz/Region.cpp | 7 ++-- tests/FlexEGT.cpp | 6 +-- tests/ModulationsT.cpp | 87 ++++++++++++++++++++++++++++++++++++++---- tests/TestHelpers.cpp | 9 ++++- tests/TestHelpers.h | 11 +++++- 5 files changed, 102 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index b48ed7f3..f7ff8798 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1169,11 +1169,10 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) return false; - if (auto value = readOpcode(opcode.value, Range { 0, 1 })) { + if (auto ampeg = readBooleanFromOpcode(opcode)) { FlexEGDescription& desc = flexEGs[egNumber - 1]; - bool ampeg = *value != 0; - if (desc.ampeg != ampeg) { - desc.ampeg = ampeg; + if (desc.ampeg != *ampeg) { + desc.ampeg = *ampeg; flexAmpEG = absl::nullopt; for (size_t i = 0, n = flexEGs.size(); i < n && !flexAmpEG; ++i) { if (flexEGs[i].ampeg) diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp index 229bee47..9d380127 100644 --- a/tests/FlexEGT.cpp +++ b/tests/FlexEGT.cpp @@ -41,7 +41,7 @@ TEST_CASE("[FlexEG] Values") REQUIRE( egDescription.points[4].time == .4_a ); REQUIRE( egDescription.points[4].level == 1.0_a ); REQUIRE( egDescription.sustain == 3 ); - REQUIRE(synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + REQUIRE(synth.getResources().modMatrix.toDotGraph() == createDefaultGraph({ R"("EG 1 {0}" -> "Amplitude {0}")", })); } @@ -67,7 +67,7 @@ TEST_CASE("[FlexEG] Default values") REQUIRE( egDescription.points[1].level == 0.0_a ); REQUIRE( egDescription.points[2].time == .1_a ); REQUIRE( egDescription.points[2].level == .25_a ); - REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({}) ); + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createDefaultGraph({}) ); } TEST_CASE("[FlexEG] Connections") @@ -85,7 +85,7 @@ TEST_CASE("[FlexEG] Connections") REQUIRE(synth.getNumRegions() == 6); REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); REQUIRE( synth.getRegionView(0)->flexEGs[0].points.size() == 2 ); - REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createDefaultGraph({ R"("EG 1 {0}" -> "Amplitude {0}")", R"("EG 1 {1}" -> "Pan {1}")", R"("EG 1 {2}" -> "Width {2}")", diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 05136501..634a92a2 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -92,7 +92,7 @@ width_oncc425=29 )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {0}")", R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {0}")", R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {0}")", @@ -111,7 +111,7 @@ TEST_CASE("[Modulations] Filter CC connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilterResonance {0, N=3}")", R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilterCutoff {0, N=2}")", R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilterGain {0, N=1}")", @@ -129,7 +129,7 @@ TEST_CASE("[Modulations] EQ CC connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "EqBandwidth {0, N=3}")", R"("Controller 2 {curve=0, smooth=0, value=5, step=0.5}" -> "EqGain {0, N=1}")", R"("Controller 3 {curve=3, smooth=0, value=300, step=0}" -> "EqFrequency {0, N=2}")", @@ -150,7 +150,7 @@ TEST_CASE("[Modulations] LFO Filter connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("LFO 1 {0}" -> "FilterCutoff {0, N=1}")", R"("LFO 2 {0}" -> "FilterCutoff {0, N=1}")", R"("LFO 3 {0}" -> "FilterResonance {0, N=1}")", @@ -174,7 +174,7 @@ TEST_CASE("[Modulations] EG Filter connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("EG 1 {0}" -> "FilterCutoff {0, N=1}")", R"("EG 2 {0}" -> "FilterCutoff {0, N=1}")", R"("EG 3 {0}" -> "FilterResonance {0, N=1}")", @@ -198,7 +198,7 @@ TEST_CASE("[Modulations] LFO EQ connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("LFO 1 {0}" -> "EqBandwidth {0, N=1}")", R"("LFO 2 {0}" -> "EqFrequency {0, N=2}")", R"("LFO 3 {0}" -> "EqGain {0, N=3}")", @@ -222,7 +222,7 @@ TEST_CASE("[Modulations] EG EQ connections") )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); - REQUIRE(graph == createReferenceGraph({ + REQUIRE(graph == createDefaultGraph({ R"("EG 1 {0}" -> "EqBandwidth {0, N=1}")", R"("EG 2 {0}" -> "EqFrequency {0, N=2}")", R"("EG 3 {0}" -> "EqGain {0, N=3}")", @@ -231,3 +231,76 @@ TEST_CASE("[Modulations] EG EQ connections") R"("EG 6 {0}" -> "EqFrequency {0, N=1}")", })); } + + +TEST_CASE("[Modulations] FlexEG Ampeg target") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=0 eg1_level1=1 + eg1_time2=1 eg1_level2=0 + eg1_time3=1 eg1_level3=.5 eg1_sustain=3 + eg1_time4=1 eg1_level4=1 + eg1_ampeg=1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("EG 1 {0}" -> "MasterAmplitude {0}")", + })); +} + +TEST_CASE("[Modulations] FlexEG Ampeg target with 2 FlexEGs") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=0 eg1_level1=1 + eg1_time2=1 eg1_level2=0 + eg1_time3=1 eg1_level3=.5 eg1_sustain=3 + eg1_time4=1 eg1_level4=1 + eg2_time1=0 eg2_level1=1 + eg2_time2=1 eg2_level2=0 + eg2_time3=1 eg2_level3=.5 eg1_sustain=3 + eg2_ampeg=1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("EG 2 {0}" -> "MasterAmplitude {0}")", + })); +} + + +TEST_CASE("[Modulations] FlexEG Ampeg target with multiple EGs targeting ampeg") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=0 eg1_level1=1 + eg1_time2=1 eg1_level2=0 + eg1_time3=1 eg1_level3=.5 eg1_sustain=3 + eg1_time4=1 eg1_level4=1 + eg1_ampeg=1 + eg2_time1=0 eg2_level1=1 + eg2_time2=1 eg2_level2=0 + eg2_time3=1 eg2_level3=.5 eg1_sustain=3 + eg2_ampeg=1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("EG 1 {0}" -> "MasterAmplitude {0}")", + })); +} + diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index 6e1fee3f..300e1cef 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -69,7 +69,7 @@ unsigned numPlayingVoices(const sfz::Synth& synth) }); } -std::string createReferenceGraph(std::vector lines, int numRegions) +std::string createDefaultGraph(std::vector lines, int numRegions) { for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { lines.push_back(absl::StrCat( @@ -87,6 +87,11 @@ std::string createReferenceGraph(std::vector lines, int numRegions) )); } + return createModulationDotGraph(lines); +}; + +std::string createModulationDotGraph(std::vector lines) +{ std::sort(lines.begin(), lines.end()); std::string graph; @@ -101,4 +106,4 @@ std::string createReferenceGraph(std::vector lines, int numRegions) graph += "}\n"; return graph; -}; +} diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index 272fb612..efc5d6d3 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -67,10 +67,17 @@ const std::vector getPlayingVoices(const sfz::Synth& synth); unsigned numPlayingVoices(const sfz::Synth& synth); /** - * @brief Create the dot graph representation from a list of strings + * @brief Create the default dot graph representation for standard regions * */ -std::string createReferenceGraph(std::vector lines, int numRegions = 1); +std::string createDefaultGraph(std::vector lines, int numRegions = 1); + +/** + * @brief Create a dot graph with the specified lines. + * The lines are sorted. + * + */ +std::string createModulationDotGraph(std::vector lines); template inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) From 49c9e43687b4230c5baf64c5508ef4ced17764ab Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 11 Aug 2020 01:26:07 +0200 Subject: [PATCH 362/445] Add runtime configs for loading in ram and voice stealing --- src/sfizz/Config.h | 1 + src/sfizz/FilePool.cpp | 48 ++++++++++++++++++++++++++++++------- src/sfizz/FilePool.h | 9 +++++++ src/sfizz/Synth.cpp | 25 +++++++++++++++++++ src/sfizz/VoiceStealing.cpp | 30 ++++++++++++++++++++++- src/sfizz/VoiceStealing.h | 23 ++++++++++++++++++ 6 files changed, 127 insertions(+), 9 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 442a165c..e99c83b3 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -33,6 +33,7 @@ namespace config { constexpr int stereoBufferPoolSize { 4 }; constexpr int indexBufferPoolSize { 2 }; constexpr int preloadSize { 8192 }; + constexpr bool loadInRam { false }; constexpr int loggerQueueSize { 256 }; constexpr int voiceLoggerQueueSize { 256 }; constexpr bool loggingEnabled { false }; diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 0fed17a0..c92ebcbd 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -261,13 +261,13 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce if (!fileInformation) return false; + fileInformation->maxOffset = maxOffset; const fs::path file { rootDirectory / fileId.filename() }; AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); - // FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same const auto frames = static_cast(reader->frames()); const auto framesToLoad = [&]() { - if (preloadSize == 0) + if (loadInRam) return frames; else return min(frames, maxOffset + preloadSize); @@ -276,6 +276,7 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce const auto existingFile = preloadedFiles.find(fileId); if (existingFile != preloadedFiles.end()) { if (framesToLoad > existingFile->second.preloadedData->getNumFrames()) { + preloadedFiles[fileId].information.maxOffset = maxOffset; preloadedFiles[fileId].preloadedData = readFromFile(*reader, framesToLoad, oversamplingFactor); } } else { @@ -350,15 +351,17 @@ sfz::FilePromisePtr sfz::FilePool::getFilePromise(const FileId& fileId) noexcept void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept { + this->preloadSize = preloadSize; + if (loadInRam) + return; + // Update all the preloaded sizes for (auto& preloadedFile : preloadedFiles) { - const auto numFrames = preloadedFile.second.preloadedData->getNumFrames() / static_cast(oversamplingFactor); - const auto maxOffset = numFrames > this->preloadSize ? static_cast(numFrames) - this->preloadSize : 0; + const auto maxOffset = preloadedFile.second.information.maxOffset; fs::path file { rootDirectory / preloadedFile.first.filename() }; AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, oversamplingFactor); } - this->preloadSize = preloadSize; } void sfz::FilePool::tryToClearPromises() @@ -473,11 +476,18 @@ void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept { float samplerateChange { static_cast(factor) / static_cast(this->oversamplingFactor) }; for (auto& preloadedFile : preloadedFiles) { - const auto numFrames = preloadedFile.second.preloadedData->getNumFrames() / static_cast(this->oversamplingFactor); - const uint32_t maxOffset = numFrames > this->preloadSize ? static_cast(numFrames) - this->preloadSize : 0; + const auto framesToLoad = [&]() { + if (loadInRam) + return preloadedFile.second.information.end; + else + return min( + preloadedFile.second.information.end, + preloadedFile.second.information.maxOffset + preloadSize + ); + }(); fs::path file { rootDirectory / preloadedFile.first.filename() }; AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); - preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, factor); + preloadedFile.second.preloadedData = readFromFile(*reader, framesToLoad, factor); preloadedFile.second.information.sampleRate *= samplerateChange; } @@ -547,3 +557,25 @@ void sfz::FilePool::raiseCurrentThreadPriority() noexcept } #endif } + +void sfz::FilePool::setRamLoading(bool loadInRam) noexcept +{ + if (loadInRam == this->loadInRam) + return; + + this->loadInRam = loadInRam; + + if (loadInRam) { + for (auto& preloadedFile : preloadedFiles) { + fs::path file { rootDirectory / preloadedFile.first.filename() }; + AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); + preloadedFile.second.preloadedData = readFromFile( + *reader, + preloadedFile.second.information.end, + oversamplingFactor + ); + } + } else { + setPreloadSize(preloadSize); + } +} diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index f05712fd..ad357ff6 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -51,6 +51,7 @@ using FileAudioBufferPtr = std::shared_ptr; struct FileInformation { uint32_t end { Default::sampleEndRange.getEnd() }; + uint32_t maxOffset { 0 }; uint32_t loopBegin { Default::loopRange.getStart() }; uint32_t loopEnd { Default::loopRange.getEnd() }; bool hasLoop { false }; @@ -269,6 +270,13 @@ public: * for background sample file processing. */ static void raiseCurrentThreadPriority() noexcept; + /** + * @brief Change whether all samples are loaded in ram. + * This will trigger a purge and reloading. + * + * @param loadInRam + */ + void setRamLoading(bool loadInRam) noexcept; private: Logger& logger; fs::path rootDirectory; @@ -279,6 +287,7 @@ private: atomic_queue::AtomicQueue2 promiseQueue; atomic_queue::AtomicQueue2 filledPromiseQueue; RTSemaphore semFilledPromiseQueueAvailable { config::maxVoices }; + bool loadInRam { config::loadInRam }; uint32_t preloadSize { config::preloadSize }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; // Signals diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 58c360ab..0041ef7d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -213,6 +213,9 @@ void sfz::Synth::clear() defaultSwitch = absl::nullopt; defaultPath = ""; resources.midiState.reset(); + resources.filePool.clear(); + resources.filePool.setRamLoading(config::loadInRam); + stealer.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::Oldest); ccLabels.clear(); keyLabels.clear(); keyswitchLabels.clear(); @@ -352,6 +355,28 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) case hash("octave_offset"): setValueFromOpcode(member, octaveOffset, Default::octaveOffsetRange); break; + case hash("hint_ram_based"): + if (member.value == "1") + resources.filePool.setRamLoading(true); + else if (member.value == "0") + resources.filePool.setRamLoading(false); + else + DBG("Unsupported value for hint_ram_based: " << member.value); + break; + case hash("hint_stealing"): + switch(hash(member.value)) { + case hash("first"): + stealer.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::First); + break; + case hash("oldest"): + stealer.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::Oldest); + break; + case hash("envelope_and_age"): + stealer.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::EnvelopeAndAge); + break; + default: + DBG("Unsupported value for hint_stealing: " << member.value); + } default: // Unsupported control opcode DBG("Unsupported control opcode: " << member.opcode); diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 7ea7c867..1c425ac7 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -10,7 +10,35 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept if (voices.empty()) return {}; - // Start of the voice stealing algorithm + switch(stealingAlgorithm) { + case StealingAlgorithm::First: + return stealFirst(voices); + case StealingAlgorithm::EnvelopeAndAge: + return stealEnvelopeAndAge(voices); + case StealingAlgorithm::Oldest: + default: + return stealOldest(voices); + } +} + +void sfz::VoiceStealing::setStealingAlgorithm(StealingAlgorithm algorithm) noexcept +{ + stealingAlgorithm = algorithm; +} + +sfz::Voice* sfz::VoiceStealing::stealFirst(absl::Span voices) noexcept +{ + return voices.front(); +} + +sfz::Voice* sfz::VoiceStealing::stealOldest(absl::Span voices) noexcept +{ + absl::c_stable_sort(voices, voiceOrdering); + return voices.front(); +} + +sfz::Voice* sfz::VoiceStealing::stealEnvelopeAndAge(absl::Span voices) noexcept +{ absl::c_stable_sort(voices, voiceOrdering); const auto sumPower = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h index 7251f37c..47eb8692 100644 --- a/src/sfizz/VoiceStealing.h +++ b/src/sfizz/VoiceStealing.h @@ -17,7 +17,25 @@ namespace sfz class VoiceStealing { public: + enum class StealingAlgorithm { + First, + Oldest, + EnvelopeAndAge + }; + VoiceStealing(); + /** + * @brief Get the current stealing algorithm + * + * @return StealingAlgorithm + */ + StealingAlgorithm getStealingAlgorithm() const noexcept { return stealingAlgorithm; } + /** + * @brief Set a default stealing algorithm + * + * @param algorithm + */ + void setStealingAlgorithm(StealingAlgorithm algorithm) noexcept; /** * @brief Propose a voice to steal from a set of voices * @@ -26,6 +44,11 @@ public: */ Voice* steal(absl::Span voices) noexcept; private: + StealingAlgorithm stealingAlgorithm { StealingAlgorithm::Oldest }; + Voice* stealFirst(absl::Span voices) noexcept; + Voice* stealOldest(absl::Span voices) noexcept; + Voice* stealEnvelopeAndAge(absl::Span voices) noexcept; + struct VoiceScore { Voice* voice; From 0142c385dcc50733879db91f587ac525276a41b8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 29 Aug 2020 14:21:25 +0200 Subject: [PATCH 363/445] Enable and disable the power follower depending on the chosen algorithm --- src/sfizz/Synth.cpp | 9 +++++++++ src/sfizz/Voice.cpp | 16 +++++++++++++++- src/sfizz/Voice.h | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 0041ef7d..0243583a 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -366,12 +366,21 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) case hash("hint_stealing"): switch(hash(member.value)) { case hash("first"): + for (auto& voice : voices) + voice->disablePowerFollower(); + stealer.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::First); break; case hash("oldest"): + for (auto& voice : voices) + voice->disablePowerFollower(); + stealer.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::Oldest); break; case hash("envelope_and_age"): + for (auto& voice : voices) + voice->enablePowerFollower(); + stealer.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::EnvelopeAndAge); break; default: diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 27323c6d..1bc55580 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -848,7 +848,10 @@ void sfz::Voice::removeVoiceFromRing() noexcept float sfz::Voice::getAveragePower() const noexcept { - return powerFollower.getAveragePower(); + if (followPower) + return powerFollower.getAveragePower(); + else + return 0.0f; } bool sfz::Voice::releasedOrFree() const noexcept @@ -1030,3 +1033,14 @@ void sfz::Voice::saveModulationTargets(const Region* region) noexcept oscillatorDetuneTarget = mm.findTarget(ModKey::createNXYZ(ModId::OscillatorDetune, region->getId())); oscillatorModDepthTarget = mm.findTarget(ModKey::createNXYZ(ModId::OscillatorModDepth, region->getId())); } + +void sfz::Voice::enablePowerFollower() noexcept +{ + followPower = true; + powerFollower.clear(); +} + +void sfz::Voice::disablePowerFollower() noexcept +{ + followPower = false; +} diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 70e5fb74..e9c41bf7 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -248,6 +248,19 @@ public: * @return float */ float getAveragePower() const noexcept; + + /** + * @brief Enable the power follower + * + */ + + void enablePowerFollower() noexcept; + /** + * @brief Disable the power follower + * + */ + void disablePowerFollower() noexcept; + /** * Returns the region that is currently playing. May be null if the voice is not active! * @@ -519,6 +532,7 @@ private: ModMatrix::TargetId oscillatorDetuneTarget; ModMatrix::TargetId oscillatorModDepthTarget; + bool followPower { false }; PowerFollower powerFollower; LEAK_DETECTOR(Voice); From 86d0cf3a49eb9a997c36989bf1d1202d77b374cb Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 5 Oct 2020 00:00:15 +0200 Subject: [PATCH 364/445] Update the boolean reader --- src/sfizz/Opcode.cpp | 18 ++++++++++-------- src/sfizz/Range.h | 12 ++++++++++++ tests/OpcodeT.cpp | 11 +++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Opcode.cpp b/src/sfizz/Opcode.cpp index 673501d1..ff6cbc6d 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -210,16 +210,18 @@ absl::optional readOpcode(absl::string_view value, const Range readBooleanFromOpcode(const Opcode& opcode) { - switch (hash(opcode.value)) { - case hash("off"): // fallthrough - case hash("0"): + // Cakewalk-style booleans, case-insensitive + if (absl::EqualsIgnoreCase(opcode.value, "off")) return false; - case hash("on"): // fallthrough - case hash("1"): + if (absl::EqualsIgnoreCase(opcode.value, "on")) return true; - default: - return {}; - } + + // ARIA-style booleans? (seen in egN_dynamic=1 for example) + // TODO check this + if (auto value = readOpcode(opcode.value, Range::wholeRange())) + return *value != 0; + + return absl::nullopt; } template diff --git a/src/sfizz/Range.h b/src/sfizz/Range.h index 3b548535..7bbe369f 100644 --- a/src/sfizz/Range.h +++ b/src/sfizz/Range.h @@ -8,6 +8,7 @@ #include "MathHelpers.h" #include #include +#include namespace sfz { @@ -116,6 +117,17 @@ public: }; } + /** + * @brief Construct a range which covers the whole numeric domain + */ + static constexpr Range wholeRange() noexcept + { + return Range { + std::numeric_limits::min(), + std::numeric_limits::max(), + }; + } + private: Type _start { static_cast(0.0) }; Type _end { static_cast(0.0) }; diff --git a/tests/OpcodeT.cpp b/tests/OpcodeT.cpp index 771db749..a2ade6a7 100644 --- a/tests/OpcodeT.cpp +++ b/tests/OpcodeT.cpp @@ -280,3 +280,14 @@ TEST_CASE("[Opcode] readOpcode") REQUIRE( !sfz::readOpcode("garbage50.25", sfz::Range(-20, 100)) ); REQUIRE( !sfz::readOpcode("garbage", sfz::Range(-20, 100)) ); } + +TEST_CASE("[Opcode] readBooleanFromOpcode") +{ + REQUIRE(sfz::readBooleanFromOpcode({"", "1"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "0"}) == false); + REQUIRE(sfz::readBooleanFromOpcode({"", "777"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "on"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "off"}) == false); + REQUIRE(sfz::readBooleanFromOpcode({"", "On"}) == true); + REQUIRE(sfz::readBooleanFromOpcode({"", "oFf"}) == false); +} From 942f93fc8de7e041b84ccd810bded4698a6a1a0a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 5 Oct 2020 19:04:25 +0200 Subject: [PATCH 365/445] Audio reader benchmark in real conditions --- benchmarks/BM_audioReaders.cpp | 164 +++++++++++++++++---------------- src/sfizz/AudioReader.cpp | 12 --- src/sfizz/AudioReader.h | 10 -- 3 files changed, 84 insertions(+), 102 deletions(-) diff --git a/benchmarks/BM_audioReaders.cpp b/benchmarks/BM_audioReaders.cpp index 874ff8d2..569a8810 100644 --- a/benchmarks/BM_audioReaders.cpp +++ b/benchmarks/BM_audioReaders.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #ifndef _WIN32 #include @@ -22,43 +23,61 @@ #endif /// -struct AutoFD { - AutoFD() {} - ~AutoFD() { reset(); } +struct TemporaryFile { + TemporaryFile(); + ~TemporaryFile(); - AutoFD(const AutoFD&) = delete; - AutoFD &operator=(const AutoFD&) = delete; + TemporaryFile(const TemporaryFile&) = delete; + TemporaryFile& operator=(const TemporaryFile&) = delete; - AutoFD(AutoFD&& other) : fd_(other.fd_) { other.fd_ = -1; } - AutoFD &operator=(AutoFD&& other) - { - if (this == &other) return *this; - reset(other.fd_); - other.fd_ = -1; - return *this; - } + TemporaryFile(TemporaryFile&&) = default; + TemporaryFile& operator=(TemporaryFile&&) = default; - explicit operator bool() const noexcept { return fd_ != -1; } - int get() const noexcept { return fd_; } - - int release() - { - int fd = fd_; - fd_ = -1; - return fd; - } - - void reset(int fd = -1) noexcept - { - if (fd_ == fd) return; - if (fd_ != -1) close(fd_); - fd_ = fd; - } + const fs::path& path() const { return path_; } private: - int fd_ = -1; + fs::path path_; + static fs::path createTemporaryFile(); }; +TemporaryFile::TemporaryFile() + : path_(createTemporaryFile()) +{ +} + +TemporaryFile::~TemporaryFile() +{ + std::error_code ec; + fs::remove(path_, ec); +} + +#if !defined(_WIN32) +fs::path TemporaryFile::createTemporaryFile() +{ + char path[] = P_tmpdir "/sndXXXXXX"; + int fd = mkstemp(path); + if (fd == -1) + throw std::runtime_error("Cannot create temporary file."); + close(fd); + return path; +} +#else +fs::path TemporaryFile::createTemporaryFile() +{ + DWORD ret = GetTempPathW(0, buffer); + std::unique_ptr path; + if (ret != 0) { + path.reset(new WCHAR[ret + 8]{}); + ret = GetTempPathW(0, buffer); + if (ret != 0) + wcscat(path.get(), L"\\XXXXXX"); + } + if (ret == 0 || !_wmktemp(path.get())) + throw std::runtime_error("Cannot create temporary file."); + return path.get(); +} +#endif + /// class AudioReaderFixture : public benchmark::Fixture { public: @@ -71,20 +90,20 @@ public: { } - static AutoFD createAudioFile(int format); + static TemporaryFile createAudioFile(int format); - static AutoFD fileWav; - static AutoFD fileFlac; - static AutoFD fileOgg; + static TemporaryFile fileWav; + static TemporaryFile fileFlac; + static TemporaryFile fileOgg; std::vector workBuffer; }; -AutoFD AudioReaderFixture::fileWav = createAudioFile(SF_FORMAT_WAV|SF_FORMAT_PCM_16); -AutoFD AudioReaderFixture::fileFlac = createAudioFile(SF_FORMAT_FLAC|SF_FORMAT_PCM_16); -AutoFD AudioReaderFixture::fileOgg = createAudioFile(SF_FORMAT_OGG|SF_FORMAT_VORBIS); +TemporaryFile AudioReaderFixture::fileWav = createAudioFile(SF_FORMAT_WAV|SF_FORMAT_PCM_16); +TemporaryFile AudioReaderFixture::fileFlac = createAudioFile(SF_FORMAT_FLAC|SF_FORMAT_PCM_16); +TemporaryFile AudioReaderFixture::fileOgg = createAudioFile(SF_FORMAT_OGG|SF_FORMAT_VORBIS); -AutoFD AudioReaderFixture::createAudioFile(int format) +TemporaryFile AudioReaderFixture::createAudioFile(int format) { constexpr unsigned sampleRate = 44100; constexpr unsigned fileDuration = 10; @@ -100,53 +119,38 @@ AutoFD AudioReaderFixture::createAudioFile(int format) phase -= static_cast(phase); } - // create anonymous temp file - FILE* file = tmpfile(); - if (!file) - throw std::system_error(errno, std::generic_category()); + // create temp file + TemporaryFile temp; + fprintf(stderr, "* Temporary file: %s\n", temp.path().u8string().c_str()); - // convert FILE to fd, for sndfile - AutoFD fd; - fd.reset(dup(fileno(file))); - if (!fd) { - fclose(file); - throw std::system_error(errno, std::generic_category()); - } - fclose(file); + // write to file +#if !defined(_WIN32) + SndfileHandle snd(temp.path().c_str(), SFM_WRITE, format, 2, sampleRate); +#else + SndfileHandle snd(temp.path().wstring().c_str(), SFM_WRITE, format, 2, sampleRate); +#endif - // write to fd - SndfileHandle snd(fd.get(), false, SFM_WRITE, format, 2, sampleRate); if (snd.error()) throw std::runtime_error("cannot open sound file for writing"); snd.writef(sndData.get(), fileFrames); snd = SndfileHandle(); - return fd; + return temp; } -static void rewindFd(int fd) +static void doReaderBenchmark(const fs::path& path, std::vector &buffer, sfz::AudioReaderType type) { -#ifndef _WIN32 - off_t off = lseek(fd, 0, SEEK_SET); -#else - off_t off = _lseek(fd, 0, SEEK_SET); -#endif - if (off == -1) - throw std::system_error(errno, std::generic_category()); -} - -static void doReaderBenchmark(int fd, std::vector &buffer, sfz::AudioReaderType type) -{ - rewindFd(fd); - sfz::AudioReaderPtr reader = sfz::createExplicitAudioReaderWithFd(fd, type); + sfz::AudioReaderPtr reader = sfz::createExplicitAudioReader(path, type); while (reader->readNextBlock(buffer.data(), buffer.size() / 2) > 0); } -static void doEntireRead(int fd) +static void doEntireRead(const fs::path& path) { - rewindFd(fd); - - SndfileHandle handle(fd, false); +#if !defined(_WIN32) + SndfileHandle handle(path.c_str()); +#else + SndfileHandle handle(path.wstring().c_str()); +#endif if (handle.error()) throw std::runtime_error("cannot open sound file for reading"); @@ -157,63 +161,63 @@ static void doEntireRead(int fd) BENCHMARK_DEFINE_F(AudioReaderFixture, EntireWav)(benchmark::State& state) { for (auto _ : state) { - doEntireRead(fileWav.get()); + doEntireRead(fileWav.path()); } } BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardWav)(benchmark::State& state) { for (auto _ : state) { - doReaderBenchmark(fileWav.get(), workBuffer, sfz::AudioReaderType::Forward); + doReaderBenchmark(fileWav.path(), workBuffer, sfz::AudioReaderType::Forward); } } BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseWav)(benchmark::State& state) { for (auto _ : state) { - doReaderBenchmark(fileWav.get(), workBuffer, sfz::AudioReaderType::Reverse); + doReaderBenchmark(fileWav.path(), workBuffer, sfz::AudioReaderType::Reverse); } } BENCHMARK_DEFINE_F(AudioReaderFixture, EntireFlac)(benchmark::State& state) { for (auto _ : state) { - doEntireRead(fileFlac.get()); + doEntireRead(fileFlac.path()); } } BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardFlac)(benchmark::State& state) { for (auto _ : state) { - doReaderBenchmark(fileFlac.get(), workBuffer, sfz::AudioReaderType::Forward); + doReaderBenchmark(fileFlac.path(), workBuffer, sfz::AudioReaderType::Forward); } } BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseFlac)(benchmark::State& state) { for (auto _ : state) { - doReaderBenchmark(fileFlac.get(), workBuffer, sfz::AudioReaderType::Reverse); + doReaderBenchmark(fileFlac.path(), workBuffer, sfz::AudioReaderType::Reverse); } } BENCHMARK_DEFINE_F(AudioReaderFixture, EntireOgg)(benchmark::State& state) { for (auto _ : state) { - doEntireRead(fileOgg.get()); + doEntireRead(fileOgg.path()); } } BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardOgg)(benchmark::State& state) { for (auto _ : state) { - doReaderBenchmark(fileOgg.get(), workBuffer, sfz::AudioReaderType::Forward); + doReaderBenchmark(fileOgg.path(), workBuffer, sfz::AudioReaderType::Forward); } } //BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseOgg)(benchmark::State& state) //{ // for (auto _ : state) { -// doReaderBenchmark(fileOgg.get(), workBuffer, sfz::AudioReaderType::Reverse); +// doReaderBenchmark(fileOgg.path(), workBuffer, sfz::AudioReaderType::Reverse); // } //} diff --git a/src/sfizz/AudioReader.cpp b/src/sfizz/AudioReader.cpp index 9122ee76..aacef1bf 100644 --- a/src/sfizz/AudioReader.cpp +++ b/src/sfizz/AudioReader.cpp @@ -333,12 +333,6 @@ AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_ return createAudioReaderWithHandle(handle, reverse, ec); } -AudioReaderPtr createAudioReaderWithFd(int fd, bool reverse, std::error_code* ec) -{ - SndfileHandle handle(fd, false); - return createAudioReaderWithHandle(handle, reverse, ec); -} - static AudioReaderPtr createExplicitAudioReaderWithHandle(SndfileHandle handle, AudioReaderType type, std::error_code* ec) { AudioReaderPtr reader; @@ -378,10 +372,4 @@ AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType t return createExplicitAudioReaderWithHandle(handle, type, ec); } -AudioReaderPtr createExplicitAudioReaderWithFd(int fd, AudioReaderType type, std::error_code* ec) -{ - SndfileHandle handle(fd, false); - return createExplicitAudioReaderWithHandle(handle, type, ec); -} - } // namespace sfz diff --git a/src/sfizz/AudioReader.h b/src/sfizz/AudioReader.h index 65a199a3..64c661f1 100644 --- a/src/sfizz/AudioReader.h +++ b/src/sfizz/AudioReader.h @@ -52,19 +52,9 @@ typedef std::unique_ptr AudioReaderPtr; */ AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec = nullptr); -/** - * @brief Create a file reader of detected type. - */ -AudioReaderPtr createAudioReaderWithFd(int fd, bool reverse, std::error_code* ec = nullptr); - /** * @brief Create a file reader of explicit type. (for testing purposes) */ AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec = nullptr); -/** - * @brief Create a file reader of explicit type. (for testing purposes) - */ -AudioReaderPtr createExplicitAudioReaderWithFd(int fd, AudioReaderType type, std::error_code* ec = nullptr); - } // namespace sfz From 9ada1cc2105e6c164acdcca0a16095e5168dc3f1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 5 Oct 2020 22:29:04 +0200 Subject: [PATCH 366/445] Modify the UI font generation process, add script --- .gitignore | 2 ++ editor/CMakeLists.txt | 2 +- ...r-20.ttf => sfizz-fluentui-system-r20.ttf} | Bin 263124 -> 275084 bytes editor/src/editor/Editor.cpp | 2 +- scripts/generate_ui_fonts.sh | 23 ++++++++++++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) rename editor/resources/Fonts/{fluentui-system-regular-20.ttf => sfizz-fluentui-system-r20.ttf} (77%) create mode 100755 scripts/generate_ui_fonts.sh diff --git a/.gitignore b/.gitignore index 8f4dc499..d9ddf04e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ clients/sfzprint /vst/download/ +/editor/external/fluentui-system-icons/ + # gh-pages unstaged files: _api/ _site/ diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 33f99afe..952a0e98 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -13,7 +13,7 @@ set(EDITOR_RESOURCES icon_white@2x.png knob48.png knob48@2x.png - Fonts/fluentui-system-regular-20.ttf + Fonts/sfizz-fluentui-system-r20.ttf Fonts/Roboto-Regular.ttf PARENT_SCOPE) diff --git a/editor/resources/Fonts/fluentui-system-regular-20.ttf b/editor/resources/Fonts/sfizz-fluentui-system-r20.ttf similarity index 77% rename from editor/resources/Fonts/fluentui-system-regular-20.ttf rename to editor/resources/Fonts/sfizz-fluentui-system-r20.ttf index 4e838582501533c67dc8a4f23293889f9795f258..dc27711e76daa93784cc68d5bf621bfbd5d258ec 100644 GIT binary patch delta 26074 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 CFontDesc("Fluent System Regular W20", fontsize)); + btn->setFont(new CFontDesc("Sfizz Fluent System R20", fontsize)); btn->setTextColor(theme->icon); btn->setHoverColor(theme->iconHighlight); btn->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); diff --git a/scripts/generate_ui_fonts.sh b/scripts/generate_ui_fonts.sh new file mode 100755 index 00000000..8b0f48f2 --- /dev/null +++ b/scripts/generate_ui_fonts.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -e + +if ! test -d "src"; then + echo "Please run this in the project root directory." + exit 1 +fi + +root="`pwd`" +fonts="$root/editor/resources/Fonts" + +if test ! -d editor/external/fluentui-system-icons; then + cd editor/external + git clone https://github.com/sfztools/fluentui-system-icons.git + cd fluentui-system-icons +else + cd editor/external/fluentui-system-icons + git checkout master + git pull origin master +fi + +./generate_icons_font.py -s regular -w 20 -n 'Sfizz Fluent System R20' \ + -o "$fonts/sfizz-fluentui-system-r20.ttf" From 8688bc6a825359f6556fb7089045e4eab8dabb28 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 5 Oct 2020 22:49:30 +0200 Subject: [PATCH 367/445] Add the installer script for Windows 7 system fonts --- scripts/innosetup.iss.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/innosetup.iss.in b/scripts/innosetup.iss.in index 7c845a4b..c17043f4 100644 --- a/scripts/innosetup.iss.in +++ b/scripts/innosetup.iss.in @@ -61,6 +61,9 @@ Source: "sfizz.vst3\Contents\@VST3_PACKAGE_ARCHITECTURE@-win\sfizz.vst3"; Compon Source: "sfizz.vst3\Contents\Resources\*"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3\Contents\Resources" Source: "sfizz.vst3\Plugin.ico"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3" Source: "sfizz.vst3\gpl-3.0.txt"; Components: main; DestDir: "{app}" +; Note(sfizz): OS older than Windows 10 require UI fonts to be installed system-wide +Source: "sfizz.vst3\Contents\Resources\Fonts\sfizz-fluentui-system-r20.ttf"; DestDir: "{autofonts}"; FontInstall: "Sfizz Fluent System R20"; Flags: uninsneveruninstall; OnlyBelowVersion: 6.4 +Source: "sfizz.vst3\Contents\Resources\Fonts\Roboto-Regular.ttf"; DestDir: "{autofonts}"; FontInstall: "Roboto Regular"; Flags: uninsneveruninstall; OnlyBelowVersion: 6.4 ;Source: "setup\vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall ; NOTE: Don't use "Flags: ignoreversion" on any shared system files From 8998b4590ac04c449db9d46c556c6b80a16f53e2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 5 Oct 2020 23:01:59 +0200 Subject: [PATCH 368/445] InnoSetup version does not support {autofonts} --- scripts/innosetup.iss.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/innosetup.iss.in b/scripts/innosetup.iss.in index c17043f4..c54d44ed 100644 --- a/scripts/innosetup.iss.in +++ b/scripts/innosetup.iss.in @@ -62,8 +62,8 @@ Source: "sfizz.vst3\Contents\Resources\*"; Components: vst3; DestDir: "{commoncf Source: "sfizz.vst3\Plugin.ico"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3" Source: "sfizz.vst3\gpl-3.0.txt"; Components: main; DestDir: "{app}" ; Note(sfizz): OS older than Windows 10 require UI fonts to be installed system-wide -Source: "sfizz.vst3\Contents\Resources\Fonts\sfizz-fluentui-system-r20.ttf"; DestDir: "{autofonts}"; FontInstall: "Sfizz Fluent System R20"; Flags: uninsneveruninstall; OnlyBelowVersion: 6.4 -Source: "sfizz.vst3\Contents\Resources\Fonts\Roboto-Regular.ttf"; DestDir: "{autofonts}"; FontInstall: "Roboto Regular"; Flags: uninsneveruninstall; OnlyBelowVersion: 6.4 +Source: "sfizz.vst3\Contents\Resources\Fonts\sfizz-fluentui-system-r20.ttf"; DestDir: "{fonts}"; FontInstall: "Sfizz Fluent System R20"; Flags: uninsneveruninstall; OnlyBelowVersion: 6.4 +Source: "sfizz.vst3\Contents\Resources\Fonts\Roboto-Regular.ttf"; DestDir: "{fonts}"; FontInstall: "Roboto Regular"; Flags: uninsneveruninstall; OnlyBelowVersion: 6.4 ;Source: "setup\vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall ; NOTE: Don't use "Flags: ignoreversion" on any shared system files From 0190450563ecad95bae14f572c6e06d98452c9c5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 5 Oct 2020 23:49:11 +0200 Subject: [PATCH 369/445] Copy plugin resources recursively --- scripts/innosetup.iss.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/innosetup.iss.in b/scripts/innosetup.iss.in index c54d44ed..4ac737bd 100644 --- a/scripts/innosetup.iss.in +++ b/scripts/innosetup.iss.in @@ -50,7 +50,7 @@ Name: "vst3"; Description: "VST3 plugin"; Types: full custom; [Files] Source: "sfizz.lv2\Contents\Binary\sfizz.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Binary"; Flags: ignoreversion Source: "sfizz.lv2\Contents\Binary\sfizz_ui.dll"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Binary"; Flags: ignoreversion -Source: "sfizz.lv2\Contents\Resources\*"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources" +Source: "sfizz.lv2\Contents\Resources\*"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2\Contents\Resources"; Flags: recursesubdirs Source: "sfizz.lv2\manifest.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\sfizz.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" Source: "sfizz.lv2\sfizz_ui.ttl"; Components: lv2; DestDir: "{commoncf}\LV2\sfizz.lv2" @@ -58,7 +58,7 @@ Source: "sfizz.lv2\lgpl-3.0.txt"; Components: main; DestDir: "{app}" Source: "sfizz.lv2\LICENSE.md"; Components: main; DestDir: "{app}" Source: "sfizz.vst3\desktop.ini"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3" Source: "sfizz.vst3\Contents\@VST3_PACKAGE_ARCHITECTURE@-win\sfizz.vst3"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3\Contents\@VST3_PACKAGE_ARCHITECTURE@-win"; Flags: ignoreversion -Source: "sfizz.vst3\Contents\Resources\*"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3\Contents\Resources" +Source: "sfizz.vst3\Contents\Resources\*"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3\Contents\Resources"; Flags: recursesubdirs Source: "sfizz.vst3\Plugin.ico"; Components: vst3; DestDir: "{commoncf}\VST3\sfizz.vst3" Source: "sfizz.vst3\gpl-3.0.txt"; Components: main; DestDir: "{app}" ; Note(sfizz): OS older than Windows 10 require UI fonts to be installed system-wide From 545e714fb6b0a7ec4bd3595a307cb53fdff96ca3 Mon Sep 17 00:00:00 2001 From: redtide Date: Mon, 28 Sep 2020 17:07:28 +0200 Subject: [PATCH 370/445] Removed old Doxygen build script --- .travis.yml | 11 ----------- .travis/update_dox.sh | 17 ----------------- 2 files changed, 28 deletions(-) delete mode 100755 .travis/update_dox.sh diff --git a/.travis.yml b/.travis.yml index 080c1cbc..d3c42bb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -166,17 +166,6 @@ jobs: install: sudo pip install git-archive-all script: git-archive-all --prefix="sfizz-${TRAVIS_BRANCH}/" -9 "${INSTALL_DIR}.tar.gz" - - name: "Generate documentation" - if: (tag IS present) AND (branch = master) AND (type = push) - addons: - apt: - packages: - - doxygen - - cmake - - libsndfile-dev - install: skip - script: .travis/update_dox.sh - - name: "Discord Webhook" install: skip script: bash ${TRAVIS_BUILD_DIR}/.travis/discord_webhook.sh success diff --git a/.travis/update_dox.sh b/.travis/update_dox.sh deleted file mode 100755 index 55ea08f4..00000000 --- a/.travis/update_dox.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -x # No fail, we need to go back to the original branch at the end -. .travis/environment.sh - -mkdir build && cd build && cmake -DSFIZZ_JACK=OFF -DSFIZZ_SHARED=OFF -DSFIZZ_LV2=OFF .. && cd .. -doxygen Doxyfile -./doxygen/scripts/generate_api_index.sh -git fetch --depth=1 https://github.com/${TRAVIS_REPO_SLUG}.git refs/heads/gh-pages:refs/remotes/origin/gh-pages -git checkout origin/gh-pages -git checkout -b gh-pages -mv _api api/${TRAVIS_TAG} -mv api_index.md api/index.md -git add api && git commit -m "Release ${TRAVIS_TAG} (Travis build: ${TRAVIS_BUILD_NUMBER})" -git remote add origin-pages https://${GITHUB_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git > /dev/null 2>&1 -git push --quiet --set-upstream origin-pages gh-pages -git checkout ${TRAVIS_BRANCH} From fd3f79cecd08cb8eadfbedcd0d02ab0f240da5fc Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 00:21:31 +0200 Subject: [PATCH 371/445] Whitespace cleanup --- scripts/doxygen/doxy2json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/doxygen/doxy2json.py b/scripts/doxygen/doxy2json.py index 6fa25bb7..4ca1aa69 100644 --- a/scripts/doxygen/doxy2json.py +++ b/scripts/doxygen/doxy2json.py @@ -179,7 +179,7 @@ for sectiondef in root.iter("sectiondef"): definition["members"] = members definitions.append(definition) - + data["definitions"] = definitions print(json.dumps(data, indent=2)) From 9835da16723b0b9cd7041ed4f67059e2196a4eff Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 00:40:53 +0200 Subject: [PATCH 372/445] Fix the condition for the source tarball task When building a release tag, the branch is not set to "master", it is the name of the release tag. Instead, do a regex match on the tag name to identify that we are processing a release. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d3c42bb2..77731c0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -156,7 +156,7 @@ jobs: - stage: "Deploy" name: "Source packaging" - if: (tag IS present) AND (branch = master) AND (type = push) + if: (tag =~ /^v?[0-9]/) AND (type = push) env: - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-src" addons: From 92045a9d389752d28c08cdee2425a281501a9bb1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 00:47:37 +0200 Subject: [PATCH 373/445] Don't include dotfiles and CI files in release tarball --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1a848588 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +.* export-ignore +.*/** export-ignore +appveyor.yml export-ignore From e7c79a9b5fad7617518a5ac23f91740c79e3daff Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 02:11:03 +0200 Subject: [PATCH 374/445] jack: use sfizz public API only --- clients/CMakeLists.txt | 2 +- clients/jack_client.cpp | 35 +++++++++++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/clients/CMakeLists.txt b/clients/CMakeLists.txt index 9192234d..80502f42 100644 --- a/clients/CMakeLists.txt +++ b/clients/CMakeLists.txt @@ -7,7 +7,7 @@ if (SFIZZ_JACK) add_executable (sfizz_jack MidiHelpers.h jack_client.cpp) target_include_directories (sfizz_jack PRIVATE ${JACK_INCLUDE_DIRS}) - target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz absl::flags_parse sfizz-sndfile ${JACK_LIBRARIES}) + target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz absl::flags_parse ${JACK_LIBRARIES}) sfizz_enable_lto_if_needed (sfizz_jack) install (TARGETS sfizz_jack DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT "jack" OPTIONAL) diff --git a/clients/jack_client.cpp b/clients/jack_client.cpp index 941d4120..b135166b 100644 --- a/clients/jack_client.cpp +++ b/clients/jack_client.cpp @@ -21,8 +21,7 @@ // (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/Synth.h" -#include "sfizz/Macros.h" +#include "sfizz.hpp" #include "MidiHelpers.h" #include #include @@ -47,9 +46,9 @@ static jack_client_t* client; int process(jack_nframes_t numFrames, void* arg) { - auto synth = reinterpret_cast(arg); + auto* synth = reinterpret_cast(arg); - auto buffer = jack_port_get_buffer(midiInputPort, numFrames); + auto* buffer = jack_port_get_buffer(midiInputPort, numFrames); assert(buffer); auto numMidiEvents = jack_midi_get_event_count(buffer); @@ -97,9 +96,11 @@ int process(jack_nframes_t numFrames, void* arg) } } - auto leftOutput = reinterpret_cast(jack_port_get_buffer(outputPort1, numFrames)); - auto rightOutput = reinterpret_cast(jack_port_get_buffer(outputPort2, numFrames)); - synth->renderBlock({ { leftOutput, rightOutput }, numFrames }); + auto* leftOutput = reinterpret_cast(jack_port_get_buffer(outputPort1, numFrames)); + auto* rightOutput = reinterpret_cast(jack_port_get_buffer(outputPort2, numFrames)); + + float* stereoOutput[] = { leftOutput, rightOutput }; + synth->renderBlock(stereoOutput, numFrames); return 0; } @@ -109,7 +110,7 @@ int sampleBlockChanged(jack_nframes_t nframes, void* arg) if (arg == nullptr) return 0; - auto synth = reinterpret_cast(arg); + auto* synth = reinterpret_cast(arg); // DBG("Sample per block changed to " << nframes); synth->setSamplesPerBlock(nframes); return 0; @@ -120,7 +121,7 @@ int sampleRateChanged(jack_nframes_t nframes, void* arg) if (arg == nullptr) return 0; - auto synth = reinterpret_cast(arg); + auto* synth = reinterpret_cast(arg); // DBG("Sample rate changed to " << nframes); synth->setSampleRate(nframes); return 0; @@ -132,7 +133,7 @@ static void done(int sig) { std::cout << "Signal received" << '\n'; shouldClose = true; - UNUSED(sig); + (void)sig; // if (client != nullptr) // exit(0); @@ -163,11 +164,11 @@ int main(int argc, char** argv) std::cout << "- Oversampling: " << oversampling << '\n'; std::cout << "- Preloaded Size: " << preload_size << '\n'; const auto factor = [&]() { - if (oversampling == "x1") return sfz::Oversampling::x1; - if (oversampling == "x2") return sfz::Oversampling::x2; - if (oversampling == "x4") return sfz::Oversampling::x4; - if (oversampling == "x8") return sfz::Oversampling::x8; - return sfz::Oversampling::x1; + if (oversampling == "x1") return 1; + if (oversampling == "x2") return 2; + if (oversampling == "x4") return 4; + if (oversampling == "x8") return 8; + return 1; }(); std::cout << "Positional arguments:"; @@ -175,7 +176,7 @@ int main(int argc, char** argv) std::cout << " " << file << ','; std::cout << '\n'; - sfz::Synth synth; + sfz::Sfizz synth; synth.setOversamplingFactor(factor); synth.setPreloadSize(preload_size); synth.loadSfzFile(filesToParse[0]); @@ -186,6 +187,7 @@ int main(int argc, char** argv) std::cout << "\tRegions: " << synth.getNumRegions() << '\n'; std::cout << "\tCurves: " << synth.getNumCurves() << '\n'; std::cout << "\tPreloadedSamples: " << synth.getNumPreloadedSamples() << '\n'; +#if 0 // not currently in public API std::cout << "==========" << '\n'; std::cout << "Included files:" << '\n'; for (auto& file : synth.getParser().getIncludedFiles()) @@ -194,6 +196,7 @@ int main(int argc, char** argv) std::cout << "Defines:" << '\n'; for (auto& define : synth.getParser().getDefines()) std::cout << '\t' << define.first << '=' << define.second << '\n'; +#endif std::cout << "==========" << '\n'; std::cout << "Unknown opcodes:"; for (auto& opcode : synth.getUnknownOpcodes()) From 59ffddaad5350c04d327ee7f466b415606ad337c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 02:12:59 +0200 Subject: [PATCH 375/445] jack: correctness of variable access in signal handler --- clients/jack_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/jack_client.cpp b/clients/jack_client.cpp index b135166b..b9e5f5dd 100644 --- a/clients/jack_client.cpp +++ b/clients/jack_client.cpp @@ -127,7 +127,7 @@ int sampleRateChanged(jack_nframes_t nframes, void* arg) return 0; } -static bool shouldClose { false }; +static volatile sig_atomic_t shouldClose { false }; static void done(int sig) { From 15c745239f1fa872abf42a01c9e089b4eae221c4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 02:23:44 +0200 Subject: [PATCH 376/445] Increase pitch ranges by 2 octaves --- src/sfizz/Defaults.h | 20 ++++++++++---------- tests/RegionT.cpp | 38 +++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 603e36b7..71473749 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -69,8 +69,8 @@ namespace Default constexpr Range oscillatorModeRange { 0, 2 }; constexpr Range oscillatorMultiRange { 1, config::oscillatorsPerVoice }; constexpr float oscillatorDetune { 0 }; - constexpr Range oscillatorDetuneRange { -9600, 9600 }; - constexpr Range oscillatorDetuneCCRange { -9600, 9600 }; + constexpr Range oscillatorDetuneRange { -12000, 12000 }; + constexpr Range oscillatorDetuneCCRange { -12000, 12000 }; constexpr float oscillatorModDepth { 0 }; constexpr Range oscillatorModDepthRange { 0, 10000 }; // depth%, allowed to be >100 for FM constexpr Range oscillatorModDepthCCRange { 0, 10000 }; @@ -166,12 +166,12 @@ namespace Default constexpr float filterResonanceCC { 0 }; constexpr float filterGainCC { 0 }; constexpr Range filterCutoffRange { 0.0f, 20000.0f }; - constexpr Range filterCutoffModRange { -9600, 9600 }; + constexpr Range filterCutoffModRange { -12000, 12000 }; constexpr Range filterGainRange { -96.0f, 96.0f }; constexpr Range filterGainModRange { -96.0f, 96.0f }; constexpr Range filterKeytrackRange { 0, 1200 }; - constexpr Range filterRandomRange { 0, 9600 }; - constexpr Range filterVeltrackRange { -9600, 9600 }; + constexpr Range filterRandomRange { 0, 12000 }; + constexpr Range filterVeltrackRange { -12000, 12000 }; constexpr Range filterResonanceRange { 0.0f, 96.0f }; constexpr Range filterResonanceModRange { 0.0f, 96.0f }; @@ -200,15 +200,15 @@ namespace Default constexpr int pitchKeytrack { 100 }; constexpr Range pitchKeytrackRange { -1200, 1200 }; constexpr float pitchRandom { 0 }; - constexpr Range pitchRandomRange { 0, 9600 }; + constexpr Range pitchRandomRange { 0, 12000 }; constexpr int pitchVeltrack { 0 }; - constexpr Range pitchVeltrackRange { -9600, 9600 }; + constexpr Range pitchVeltrackRange { -12000, 12000 }; constexpr int transpose { 0 }; constexpr Range transposeRange { -127, 127 }; constexpr int tune { 0 }; - constexpr Range tuneRange { -9600, 9600 }; // ±100 in SFZv1, more in ARIA - constexpr Range tuneCCRange { -9600, 9600 }; - constexpr Range bendBoundRange { -9600, 9600 }; + constexpr Range tuneRange { -12000, 12000 }; // ±100 in SFZv1, more in ARIA + constexpr Range tuneCCRange { -12000, 12000 }; + constexpr Range bendBoundRange { -12000, 12000 }; constexpr Range bendStepRange { 1, 1200 }; constexpr int bendUp { 200 }; // No range here because the bounds can be inverted constexpr int bendDown { -200 }; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index b07f4816..4e8419e1 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -960,8 +960,8 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.pitchRandom == 40); region.parseOpcode({ "pitch_random", "-1" }); REQUIRE(region.pitchRandom == 0); - region.parseOpcode({ "pitch_random", "10320" }); - REQUIRE(region.pitchRandom == 9600); + region.parseOpcode({ "pitch_random", "12320" }); + REQUIRE(region.pitchRandom == 12000); } SECTION("pitch_veltrack") @@ -972,9 +972,9 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "pitch_veltrack", "-1" }); REQUIRE(region.pitchVeltrack == -1); region.parseOpcode({ "pitch_veltrack", "13020" }); - REQUIRE(region.pitchVeltrack == 9600); + REQUIRE(region.pitchVeltrack == 12000); region.parseOpcode({ "pitch_veltrack", "-13020" }); - REQUIRE(region.pitchVeltrack == -9600); + REQUIRE(region.pitchVeltrack == -12000); } SECTION("transpose") @@ -998,9 +998,9 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "tune", "-1" }); REQUIRE(region.tune == -1); region.parseOpcode({ "tune", "15432" }); - REQUIRE(region.tune == 9600); + REQUIRE(region.tune == 12000); region.parseOpcode({ "tune", "-15432" }); - REQUIRE(region.tune == -9600); + REQUIRE(region.tune == -12000); } SECTION("bend_up, bend_down, bend_step, bend_smooth") @@ -1012,18 +1012,18 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.bendUp == 400); region.parseOpcode({ "bend_up", "-200" }); REQUIRE(region.bendUp == -200); - region.parseOpcode({ "bend_up", "9700" }); - REQUIRE(region.bendUp == 9600); - region.parseOpcode({ "bend_up", "-9700" }); - REQUIRE(region.bendUp == -9600); + region.parseOpcode({ "bend_up", "12700" }); + REQUIRE(region.bendUp == 12000); + region.parseOpcode({ "bend_up", "-12700" }); + REQUIRE(region.bendUp == -12000); region.parseOpcode({ "bend_down", "400" }); REQUIRE(region.bendDown == 400); region.parseOpcode({ "bend_down", "-200" }); REQUIRE(region.bendDown == -200); - region.parseOpcode({ "bend_down", "9700" }); - REQUIRE(region.bendDown == 9600); - region.parseOpcode({ "bend_down", "-9700" }); - REQUIRE(region.bendDown == -9600); + region.parseOpcode({ "bend_down", "12700" }); + REQUIRE(region.bendDown == 12000); + region.parseOpcode({ "bend_down", "-12700" }); + REQUIRE(region.bendDown == -12000); region.parseOpcode({ "bend_step", "400" }); REQUIRE(region.bendStep == 400); region.parseOpcode({ "bend_step", "-200" }); @@ -1389,10 +1389,10 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.filters[0].veltrack == 50); region.parseOpcode({ "fil_veltrack", "-5" }); REQUIRE(region.filters[0].veltrack == -5); - region.parseOpcode({ "fil_veltrack", "10000" }); - REQUIRE(region.filters[0].veltrack == 9600); - region.parseOpcode({ "fil_veltrack", "-10000" }); - REQUIRE(region.filters[0].veltrack == -9600); + region.parseOpcode({ "fil_veltrack", "13000" }); + REQUIRE(region.filters[0].veltrack == 12000); + region.parseOpcode({ "fil_veltrack", "-13000" }); + REQUIRE(region.filters[0].veltrack == -12000); REQUIRE(region.filters[0].keycenter == 60); region.parseOpcode({ "fil_keycenter", "50" }); @@ -1729,7 +1729,7 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "tune_stepcc120", "24" }); REQUIRE(view.at(120).step == 24.0f); region.parseOpcode({ "pitch_stepcc120", "15482" }); - REQUIRE(view.at(120).step == 9600.0f); + REQUIRE(view.at(120).step == 12000.0f); region.parseOpcode({ "tune_stepcc120", "-2" }); REQUIRE(view.at(120).step == 0.0f); } From b0e102c77ef246dd102ba7555669af5d5f0a6f05 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 03:53:32 +0200 Subject: [PATCH 377/445] Fix CC modulations, having their source depth to 0 --- src/sfizz/Region.cpp | 4 +-- src/sfizz/Synth.cpp | 8 ++--- src/sfizz/modulations/ModKey.cpp | 5 ++- src/sfizz/modulations/ModKey.h | 4 +-- src/sfizz/modulations/ModKeyHash.cpp | 1 - src/sfizz/modulations/sources/Controller.cpp | 2 +- tests/FilesT.cpp | 2 +- tests/ModulationsT.cpp | 32 ++++++++++---------- tests/RegionT.cpp | 22 +++++++------- tests/TestHelpers.cpp | 16 ++++++++-- tests/TestHelpers.h | 1 + 11 files changed, 54 insertions(+), 43 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index f98f3ef8..15e183cf 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1550,7 +1550,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con else { connections.emplace_back(); conn = &connections.back(); - conn->source = ModKey::createCC(ccNumber, 0, 0, 0, 0); + conn->source = ModKey::createCC(ccNumber, 0, 0, 0); conn->target = target; } @@ -1558,7 +1558,7 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, con ModKey::Parameters p = conn->source.parameters(); switch (opcode.category) { case kOpcodeOnCcN: - setValueFromOpcode(opcode, p.value, range); + setValueFromOpcode(opcode, conn->sourceDepth, range); break; case kOpcodeCurveCcN: setValueFromOpcode(opcode, p.curve, Default::curveCCRange); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 537e7d6a..94beb566 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -145,11 +145,11 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) // Create default connections constexpr unsigned defaultSmoothness = 10; lastRegion->getOrCreateConnection( - ModKey::createCC(7, 4, defaultSmoothness, 100, 0), - ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 1.0f; + ModKey::createCC(7, 4, defaultSmoothness, 0), + ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 100.0f; lastRegion->getOrCreateConnection( - ModKey::createCC(10, 1, defaultSmoothness, 100, 0), - ModKey::createNXYZ(ModId::Pan, lastRegion->id)).sourceDepth = 1.0f; + ModKey::createCC(10, 1, defaultSmoothness, 0), + ModKey::createNXYZ(ModId::Pan, lastRegion->id)).sourceDepth = 100.0f; // auto parseOpcodes = [&](const std::vector& opcodes) { diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 5fa48181..9750070c 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -30,13 +30,12 @@ ModKey::Parameters& ModKey::Parameters::operator=(const Parameters& other) noexc return *this; } -ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step) +ModKey ModKey::createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float step) { ModKey::Parameters p; p.cc = cc; p.curve = curve; p.smooth = smooth; - p.value = value; p.step = step; return ModKey(ModId::Controller, {}, p); } @@ -69,7 +68,7 @@ std::string ModKey::toString() const case ModId::Controller: return absl::StrCat("Controller ", params_.cc, " {curve=", params_.curve, ", smooth=", params_.smooth, - ", value=", params_.value, ", step=", params_.step, "}"); + ", step=", params_.step, "}"); case ModId::Envelope: return absl::StrCat("EG ", 1 + params_.N, " {", region_.number(), "}"); case ModId::LFO: diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index aec4f5ea..e3f484c4 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -28,7 +28,7 @@ public: explicit ModKey(ModId id, NumericId region = {}, Parameters params = {}) : id_(id), region_(region), params_(params), flags_(ModIds::flags(id_)) {} - static ModKey createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float value, float step); + static ModKey createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float step); static ModKey createNXYZ(ModId id, NumericId region, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0); explicit operator bool() const noexcept { return id_ != ModId(); } @@ -62,7 +62,7 @@ public: union { //! Parameters if this key identifies a CC source - struct { uint16_t cc; uint8_t curve, smooth; float value, step; }; + struct { uint16_t cc; uint8_t curve, smooth; float step; }; //! Parameters otherwise, based on the related opcode // eg. `N` in `lfoN`, `N, X` in `lfoN_eqX` struct { uint8_t N, X, Y, Z; }; diff --git a/src/sfizz/modulations/ModKeyHash.cpp b/src/sfizz/modulations/ModKeyHash.cpp index 8e274a45..beeb90d3 100644 --- a/src/sfizz/modulations/ModKeyHash.cpp +++ b/src/sfizz/modulations/ModKeyHash.cpp @@ -20,7 +20,6 @@ size_t std::hash::operator()(const sfz::ModKey &key) const k = hashNumber(p.cc, k); k = hashNumber(p.curve, k); k = hashNumber(p.smooth, k); - k = hashNumber(p.value, k); k = hashNumber(p.step, k); break; default: diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp index 1e611220..c81229f2 100644 --- a/src/sfizz/modulations/sources/Controller.cpp +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -76,7 +76,7 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceI const EventVector& events = ms.getCCEvents(p.cc); auto transformValue = [p, &curve](float x) { - return curve.evalNormalized(x) * p.value; + return curve.evalNormalized(x); }; if (p.step > 0.0f) diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index dbff1ba1..33360239 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -416,7 +416,7 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines") const ModKey target = ModKey::createNXYZ(ModId::Amplitude, synth.getRegionView(2)->getId()); const RegionCCView view(*synth.getRegionView(2), target); REQUIRE(!view.empty()); - REQUIRE(view.at(10).value == 34.0f); + REQUIRE(view.valueAt(10) == 34.0f); } TEST_CASE("[Files] Specific bug: relative path with backslashes") diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 634a92a2..9b6c18e6 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -93,10 +93,10 @@ width_oncc425=29 const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createDefaultGraph({ - R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {0}")", - R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {0}")", - R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {0}")", - R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {0}")", + R"("Controller 20 {curve=3, smooth=0, step=0}" -> "Amplitude {0}")", + R"("Controller 42 {curve=0, smooth=32, step=0}" -> "Pitch {0}")", + R"("Controller 36 {curve=0, smooth=0, step=1.5}" -> "Pan {0}")", + R"("Controller 425 {curve=0, smooth=0, step=0}" -> "Width {0}")", })); } @@ -112,9 +112,9 @@ TEST_CASE("[Modulations] Filter CC connections") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createDefaultGraph({ - R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilterResonance {0, N=3}")", - R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilterCutoff {0, N=2}")", - R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilterGain {0, N=1}")", + R"("Controller 1 {curve=0, smooth=10, step=0}" -> "FilterResonance {0, N=3}")", + R"("Controller 2 {curve=2, smooth=0, step=0}" -> "FilterCutoff {0, N=2}")", + R"("Controller 3 {curve=0, smooth=0, step=0.5}" -> "FilterGain {0, N=1}")", })); } @@ -130,9 +130,9 @@ TEST_CASE("[Modulations] EQ CC connections") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createDefaultGraph({ - R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "EqBandwidth {0, N=3}")", - R"("Controller 2 {curve=0, smooth=0, value=5, step=0.5}" -> "EqGain {0, N=1}")", - R"("Controller 3 {curve=3, smooth=0, value=300, step=0}" -> "EqFrequency {0, N=2}")", + R"("Controller 1 {curve=0, smooth=10, step=0}" -> "EqBandwidth {0, N=3}")", + R"("Controller 2 {curve=0, smooth=0, step=0.5}" -> "EqGain {0, N=1}")", + R"("Controller 3 {curve=3, smooth=0, step=0}" -> "EqFrequency {0, N=2}")", })); } @@ -248,8 +248,8 @@ TEST_CASE("[Modulations] FlexEG Ampeg target") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createModulationDotGraph({ - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("EG 1 {0}" -> "MasterAmplitude {0}")", })); } @@ -272,8 +272,8 @@ TEST_CASE("[Modulations] FlexEG Ampeg target with 2 FlexEGs") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createModulationDotGraph({ - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("EG 2 {0}" -> "MasterAmplitude {0}")", })); } @@ -298,8 +298,8 @@ TEST_CASE("[Modulations] FlexEG Ampeg target with multiple EGs targeting ampeg") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createModulationDotGraph({ - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {0}")", - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {0}")", + R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("EG 1 {0}" -> "MasterAmplitude {0}")", })); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 4e8419e1..bf914d36 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -572,7 +572,7 @@ TEST_CASE("[Region] Parsing opcodes") const RegionCCView view(region, target); REQUIRE(view.empty()); region.parseOpcode({ "pan_oncc45", "4.2" }); - REQUIRE(view.at(45).value == 4.2_a); + REQUIRE(view.valueAt(45) == 4.2_a); region.parseOpcode({ "pan_curvecc17", "18" }); REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "pan_curvecc17", "15482" }); @@ -612,7 +612,7 @@ TEST_CASE("[Region] Parsing opcodes") const RegionCCView view(region, target); REQUIRE(view.empty()); region.parseOpcode({ "width_oncc45", "4.2" }); - REQUIRE(view.at(45).value == 4.2_a); + REQUIRE(view.valueAt(45) == 4.2_a); region.parseOpcode({ "width_curvecc17", "18" }); REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "width_curvecc17", "15482" }); @@ -652,7 +652,7 @@ TEST_CASE("[Region] Parsing opcodes") const RegionCCView view(region, target); REQUIRE(view.empty()); region.parseOpcode({ "position_oncc45", "4.2" }); - REQUIRE(view.at(45).value == 4.2_a); + REQUIRE(view.valueAt(45) == 4.2_a); region.parseOpcode({ "position_curvecc17", "18" }); REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "position_curvecc17", "15482" }); @@ -1649,9 +1649,9 @@ TEST_CASE("[Region] Parsing opcodes") const RegionCCView view(region, target); REQUIRE(view.empty()); region.parseOpcode({ "amplitude_cc1", "40" }); - REQUIRE(view.at(1).value == 40.0_a); + REQUIRE(view.valueAt(1) == 40.0_a); region.parseOpcode({ "amplitude_oncc2", "30" }); - REQUIRE(view.at(2).value == 30.0_a); + REQUIRE(view.valueAt(2) == 30.0_a); region.parseOpcode({ "amplitude_curvecc17", "18" }); REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "amplitude_curvecc17", "15482" }); @@ -1678,11 +1678,11 @@ TEST_CASE("[Region] Parsing opcodes") const RegionCCView view(region, target); REQUIRE(view.empty()); region.parseOpcode({ "gain_cc1", "40" }); - REQUIRE(view.at(1).value == 40_a); + REQUIRE(view.valueAt(1) == 40_a); region.parseOpcode({ "volume_oncc2", "-76" }); - REQUIRE(view.at(2).value == -76.0_a); + REQUIRE(view.valueAt(2) == -76.0_a); region.parseOpcode({ "gain_oncc4", "-1" }); - REQUIRE(view.at(4).value == -1.0_a); + REQUIRE(view.valueAt(4) == -1.0_a); region.parseOpcode({ "volume_curvecc17", "18" }); REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "volume_curvecc17", "15482" }); @@ -1709,11 +1709,11 @@ TEST_CASE("[Region] Parsing opcodes") const RegionCCView view(region, target); REQUIRE(view.empty()); region.parseOpcode({ "pitch_cc1", "40" }); - REQUIRE(view.at(1).value == 40.0); + REQUIRE(view.valueAt(1) == 40.0); region.parseOpcode({ "tune_oncc2", "-76" }); - REQUIRE(view.at(2).value == -76.0); + REQUIRE(view.valueAt(2) == -76.0); region.parseOpcode({ "pitch_oncc4", "-1" }); - REQUIRE(view.at(4).value == -1.0); + REQUIRE(view.valueAt(4) == -1.0); region.parseOpcode({ "tune_curvecc17", "18" }); REQUIRE(view.at(17).curve == 18); region.parseOpcode({ "pitch_curvecc17", "15482" }); diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index 300e1cef..c5b3691d 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -35,6 +35,18 @@ sfz::ModKey::Parameters RegionCCView::at(int cc) const throw std::out_of_range("Region CC"); } +float RegionCCView::valueAt(int cc) const +{ + for (const sfz::Region::Connection& conn : region_.connections) { + if (match(conn)) { + const sfz::ModKey::Parameters p = conn.source.parameters(); + if (p.cc == cc) + return conn.sourceDepth; + } + } + throw std::out_of_range("Region CC"); +} + bool RegionCCView::match(const sfz::Region::Connection& conn) const { return conn.source.id() == sfz::ModId::Controller && conn.target == target_; @@ -76,12 +88,12 @@ std::string createDefaultGraph(std::vector lines, int numRegions) R"("AmplitudeEG {)", regionIdx, R"(}" -> "MasterAmplitude {)", regionIdx, R"(}")" )); lines.push_back(absl::StrCat( - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {)", + R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {)", regionIdx, R"(}")" )); lines.push_back(absl::StrCat( - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {)", + R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {)", regionIdx, R"(}")" )); diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index efc5d6d3..4d059c45 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -19,6 +19,7 @@ public: size_t size() const; bool empty() const; sfz::ModKey::Parameters at(int cc) const; + float valueAt(int cc) const; private: bool match(const sfz::Region::Connection& conn) const; From 99d40ee31cfb870f36e27a3c55dcfb69c588e930 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 04:23:19 +0200 Subject: [PATCH 378/445] Normalize stepcc in the source key --- src/sfizz/Synth.cpp | 17 ++++++++++++++--- tests/ModulationsT.cpp | 8 ++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 94beb566..645f6168 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1522,7 +1522,18 @@ void sfz::Synth::setupModMatrix() for (const Region::Connection& conn : region->connections) { ModGenerator* gen = nullptr; - switch (conn.source.id()) { + ModKey sourceKey = conn.source; + ModKey targetKey = conn.target; + + // normalize the stepcc to 0-1 + if (sourceKey.id() == ModId::Controller) { + ModKey::Parameters p = sourceKey.parameters(); + p.step = (conn.sourceDepth == 0.0f) ? 0.0f : + (p.step / conn.sourceDepth); + sourceKey = ModKey::createCC(p.cc, p.curve, p.smooth, p.step); + } + + switch (sourceKey.id()) { case ModId::Controller: gen = genController.get(); break; @@ -1546,8 +1557,8 @@ void sfz::Synth::setupModMatrix() if (!gen) continue; - ModMatrix::SourceId source = mm.registerSource(conn.source, *gen); - ModMatrix::TargetId target = mm.registerTarget(conn.target); + ModMatrix::SourceId source = mm.registerSource(sourceKey, *gen); + ModMatrix::TargetId target = mm.registerTarget(targetKey); ASSERT(source); if (!source) { diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 9b6c18e6..9a617b48 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -87,7 +87,7 @@ TEST_CASE("[Modulations] Connection graph from SFZ") sample=*sine amplitude_oncc20=59 amplitude_curvecc20=3 pitch_oncc42=71 pitch_smoothcc42=32 -pan_oncc36=14.5 pan_stepcc36=1.5 +pan_oncc36=12.5 pan_stepcc36=0.5 width_oncc425=29 )"); @@ -95,7 +95,7 @@ width_oncc425=29 REQUIRE(graph == createDefaultGraph({ R"("Controller 20 {curve=3, smooth=0, step=0}" -> "Amplitude {0}")", R"("Controller 42 {curve=0, smooth=32, step=0}" -> "Pitch {0}")", - R"("Controller 36 {curve=0, smooth=0, step=1.5}" -> "Pan {0}")", + R"("Controller 36 {curve=0, smooth=0, step=0.04}" -> "Pan {0}")", R"("Controller 425 {curve=0, smooth=0, step=0}" -> "Width {0}")", })); } @@ -114,7 +114,7 @@ TEST_CASE("[Modulations] Filter CC connections") REQUIRE(graph == createDefaultGraph({ R"("Controller 1 {curve=0, smooth=10, step=0}" -> "FilterResonance {0, N=3}")", R"("Controller 2 {curve=2, smooth=0, step=0}" -> "FilterCutoff {0, N=2}")", - R"("Controller 3 {curve=0, smooth=0, step=0.5}" -> "FilterGain {0, N=1}")", + R"("Controller 3 {curve=0, smooth=0, step=0.1}" -> "FilterGain {0, N=1}")", })); } @@ -131,7 +131,7 @@ TEST_CASE("[Modulations] EQ CC connections") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createDefaultGraph({ R"("Controller 1 {curve=0, smooth=10, step=0}" -> "EqBandwidth {0, N=3}")", - R"("Controller 2 {curve=0, smooth=0, step=0.5}" -> "EqGain {0, N=1}")", + R"("Controller 2 {curve=0, smooth=0, step=0.1}" -> "EqGain {0, N=1}")", R"("Controller 3 {curve=3, smooth=0, step=0}" -> "EqFrequency {0, N=2}")", })); } From 6fc1d470ff9d472c683c7ee1be644141f57c00c3 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 03:03:52 +0200 Subject: [PATCH 379/445] Do not connect CC 7 and 10 if used in SFZ file --- src/sfizz/Synth.cpp | 36 +++++++++++++++++++++++++++--------- tests/ModulationsT.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 645f6168..4919f01c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -142,15 +142,6 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) int regionNumber = static_cast(regions.size()); auto lastRegion = absl::make_unique(regionNumber, resources.midiState, defaultPath); - // Create default connections - constexpr unsigned defaultSmoothness = 10; - lastRegion->getOrCreateConnection( - ModKey::createCC(7, 4, defaultSmoothness, 0), - ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 100.0f; - lastRegion->getOrCreateConnection( - ModKey::createCC(10, 1, defaultSmoothness, 0), - ModKey::createNXYZ(ModId::Pan, lastRegion->id)).sourceDepth = 100.0f; - // auto parseOpcodes = [&](const std::vector& opcodes) { for (auto& opcode : opcodes) { @@ -626,6 +617,33 @@ void sfz::Synth::finalizeSfzLoad() } DBG("Removing " << (regions.size() - currentRegionCount) << " out of " << regions.size() << " regions"); regions.resize(currentRegionCount); + + // collect all CCs used in regions, with matrix not yet connected + std::bitset usedCCs; + for (const RegionPtr& regionPtr : regions) { + const Region& region = *regionPtr; + updateUsedCCsFromRegion(usedCCs, region); + for (const Region::Connection& connection : region.connections) { + if (connection.source.id() == ModId::Controller) + usedCCs.set(connection.source.parameters().cc); + } + } + // connect default controllers, except if these CC are already used + for (const RegionPtr& regionPtr : regions) { + Region& region = *regionPtr; + constexpr unsigned defaultSmoothness = 10; + if (!usedCCs.test(7)) { + region.getOrCreateConnection( + ModKey::createCC(7, 4, defaultSmoothness, 0), + ModKey::createNXYZ(ModId::Amplitude, region.id)).sourceDepth = 100.0f; + } + if (!usedCCs.test(10)) { + region.getOrCreateConnection( + ModKey::createCC(10, 1, defaultSmoothness, 0), + ModKey::createNXYZ(ModId::Pan, region.id)).sourceDepth = 100.0f; + } + } + modificationTime = checkModificationTime(); settingsPerVoice.maxFilters = maxFilters; diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 9a617b48..d5dc49af 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -304,3 +304,33 @@ TEST_CASE("[Modulations] FlexEG Ampeg target with multiple EGs targeting ampeg") })); } +TEST_CASE("[Modulations] Override the default volume controller") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine tune_oncc7=1200 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("AmplitudeEG {0}" -> "MasterAmplitude {0}")", + R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 7 {curve=0, smooth=0, step=0}" -> "Pitch {0}")", + })); +} + +TEST_CASE("[Modulations] Override the default pan controller") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine on_locc10=127 on_hicc10=127 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createModulationDotGraph({ + R"("AmplitudeEG {0}" -> "MasterAmplitude {0}")", + R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", + })); +} From 049739f8f1550ec3e79c45dd57318db5abf0ee0f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 10:49:41 +0200 Subject: [PATCH 380/445] Use the correct coeffs for xfIn --- src/sfizz/Voice.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 8f655f77..b1522174 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -707,12 +707,12 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept source, ptBuffer, ptIndices, ptCoeffs, {}, quality); if (ptType == kPartitionLoopXfade) { - absl::Span xfCoeff = xfadeTemp[0]->first(ptSize); + absl::Span xfCurvePos = xfadeTemp[0]->first(ptSize); - // compute crossfade coeffs + // compute crossfade positions for (unsigned i = 0; i < ptSize; ++i) { float pos = ptIndices[i] + ptCoeffs[i]; - xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; + xfCurvePos[i] = (pos - loopXfOutStart) / loopXfadeSize; } //----------------------------------------------------------------// @@ -724,17 +724,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = xfIn.evalNormalized(1.0f - xfCoeff[i]); + xfCurve[i] = xfIn.evalNormalized(1.0f - xfCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfOut = resources.curves.getCurve(6); for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + xfCurve[i] = xfOut.evalNormalized(xfCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = clamp(1.0f - xfCoeff[i], 0.0f, 1.0f); + xfCurve[i] = clamp(1.0f - xfCurvePos[i], 0.0f, 1.0f); } // apply out curve if (0) @@ -765,10 +765,11 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept ++applyOffset; unsigned applySize = ptSize - applyOffset; - // offset the indices + // offset the indices and coeffs xfInIndices = xfInIndices.subspan(applyOffset); - // offset the coeffs - absl::Span xfInCoeff = xfCoeff.subspan(applyOffset); + absl::Span xfInCoeffs = ptCoeffs.subspan(applyOffset); + // offset the curve positions + absl::Span xfInCurvePos = xfCurvePos.subspan(applyOffset); // offset the output buffer AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); @@ -777,21 +778,21 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfIn = resources.curves.getCurve(5); for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = clamp(xfInCoeff[i], 0.0f, 1.0f); + xfCurve[i] = clamp(xfInCurvePos[i], 0.0f, 1.0f); } // apply in curve fillInterpolatedWithQuality( - source, xfInBuffer, xfInIndices, *coeffs, xfCurve, quality); + source, xfInBuffer, xfInIndices, xfInCoeffs, xfCurve, quality); } } } From 9f698aea4cf3b1d6a55c9ac2de34674b91923ae7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 11:54:02 +0200 Subject: [PATCH 381/445] Remove dead code --- src/sfizz/Voice.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index b1522174..654dba5b 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -737,16 +737,12 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept xfCurve[i] = clamp(1.0f - xfCurvePos[i], 0.0f, 1.0f); } // apply out curve - if (0) - ptBuffer.applyGain(xfCurve); - else { - // scalar fallback: buffer and curve not aligned - size_t numChannels = ptBuffer.getNumChannels(); - for (size_t c = 0; c < numChannels; ++c) { - absl::Span channel = ptBuffer.getSpan(c); - for (unsigned i = 0; i < ptSize; ++i) - channel[i] *= xfCurve[i]; - } + // (scalar fallback: buffer and curve not aligned) + size_t numChannels = ptBuffer.getNumChannels(); + for (size_t c = 0; c < numChannels; ++c) { + absl::Span channel = ptBuffer.getSpan(c); + for (unsigned i = 0; i < ptSize; ++i) + channel[i] *= xfCurve[i]; } } //----------------------------------------------------------------// From 35bc3f59b34469255d2cd8564827c6dc795523e1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 15:32:30 +0200 Subject: [PATCH 382/445] lv2: remove the duplicate port --- lv2/sfizz.ttl.in | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index cddda959..5ddd0317 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -313,17 +313,6 @@ midnam:update a lv2:Feature . lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 256 ; - ] , [ - a lv2:OutputPort, lv2:ControlPort ; - lv2:index 12 ; - lv2:symbol "active_voices" ; - lv2:name "Active voices", - "Voix utilisées"@fr ; - pg:group <@LV2PLUGIN_URI@#status> ; - lv2:portProperty lv2:integer ; - lv2:default 0 ; - lv2:minimum 0 ; - lv2:maximum 256 ; ] , [ a lv2:OutputPort, lv2:ControlPort ; lv2:index 13 ; From 8f3be5ca3c55da22896516fa2a01a7ada004b01e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 5 Oct 2020 13:48:21 +0200 Subject: [PATCH 383/445] Compute loop information at the start of the voice The check for actual looping is still done in the render method, to see if we do have enough samples --- src/sfizz/Voice.cpp | 63 +++++++++++++++++++++++++++++---------------- src/sfizz/Voice.h | 19 ++++++++++++++ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 654dba5b..c1b05e8a 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -99,6 +99,7 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event switchState(State::cleanMeUp); return; } + updateLoopInformation(); speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); } @@ -558,26 +559,11 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } // calculate loop characteristics - bool isLooping = false; - int loopStart = 0; - int loopEnd = 0; - int loopSize = 0; - int loopXfadeSize = 0; - int loopXfOutStart = 0; - int loopXfInStart = 0; // Note: beware in case of negative index + const bool isLooping = region->shouldLoop() + && (static_cast(loop.end) < source.getNumFrames()); SpanHolder> xfadeTemp[2]; SpanHolder> xfadeIndexTemp[1]; - if (region->shouldLoop()) { - loopEnd = region->loopEnd(currentPromise->oversamplingFactor); - isLooping = static_cast(loopEnd) < source.getNumFrames(); - } if (isLooping) { - loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); - loopSize = loopEnd + 1 - loopStart; - loopXfadeSize = static_cast( - lroundPositive(region->loopCrossfade * static_cast(currentPromise->oversamplingFactor) * currentPromise->sampleRate)); - loopXfOutStart = loopEnd + 1 - loopXfadeSize; - loopXfInStart = loopStart - loopXfadeSize; for (auto& buf : xfadeTemp) { buf = resources.bufferPool.getBuffer(numSamples); if (!buf) @@ -642,12 +628,12 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept int index = (*indices)[i]; // wrap indices post loop-entry around the loop segment - int wrappedIndex = (index <= loopEnd) ? index : - (loopStart + (index - loopStart) % loopSize); + int wrappedIndex = (index <= loop.end) ? index : + (loop.start + (index - loop.start) % loop.size); (*indices)[i] = wrappedIndex; // identify the partition this index is in - bool xfading = wrappedIndex >= loopStart && wrappedIndex >= loopXfOutStart; + bool xfading = wrappedIndex >= loop.start && wrappedIndex >= loop.xfOutStart; int partitionType = xfading ? kPartitionLoopXfade : kPartitionNormal; // if looping or entering a different type, start a new partition bool start = i == 0 || wrappedIndex < oldIndex || partitionType != oldPartitionType; @@ -712,7 +698,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute crossfade positions for (unsigned i = 0; i < ptSize; ++i) { float pos = ptIndices[i] + ptCoeffs[i]; - xfCurvePos[i] = (pos - loopXfOutStart) / loopXfadeSize; + xfCurvePos[i] = (pos - loop.xfOutStart) / loop.xfSize; } //----------------------------------------------------------------// @@ -752,7 +738,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute indices of the crossfade input segment absl::Span xfInIndices = xfadeIndexTemp[0]->first(ptSize); absl::c_copy(ptIndices, xfInIndices.begin()); - subtract1(loopXfOutStart - loopXfInStart, xfInIndices); + subtract1(loop.xfOutStart - loop.xfInStart, xfInIndices); // disregard the segment whose indices have been pushed // into the negatives, take these virtually as zeroes. @@ -1067,6 +1053,8 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; + resetLoopInformation(); + powerFollower.clear(); for (auto& filter : filters) @@ -1078,6 +1066,37 @@ void sfz::Voice::reset() noexcept removeVoiceFromRing(); } +void sfz::Voice::resetLoopInformation() noexcept +{ + loop.start = 0; + loop.end = 0; + loop.size = 0; + loop.xfSize = 0; + loop.xfOutStart = 0; + loop.xfInStart = 0; +} + +void sfz::Voice::updateLoopInformation() noexcept +{ + if (!region || !currentPromise) + return; + + if (!region->shouldLoop()) + return; + + const auto factor = currentPromise->oversamplingFactor; + const auto rate = currentPromise->sampleRate; + + loop.end = static_cast(region->loopEnd(factor)); + loop.start = static_cast(region->loopStart(factor)); + loop.size = loop.end + 1 - loop.start; + loop.xfSize = static_cast( + lroundPositive(region->loopCrossfade * static_cast(factor) * rate) + ); + loop.xfOutStart = loop.end + 1 - loop.xfSize; + loop.xfInStart = loop.start - loop.xfSize; +} + void sfz::Voice::setNextSisterVoice(Voice* voice) noexcept { // Should never be null diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 95dbd9ec..1c1d2d86 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -496,6 +496,25 @@ private: int sourcePosition { 0 }; int initialDelay { 0 }; int age { 0 }; + struct { + int start { 0 }; + int end { 0 }; + int size { 0 }; + int xfSize { 0 }; + int xfOutStart { 0 }; + int xfInStart { 0 }; + } loop; + /** + * @brief Reset the loop information + * + */ + void resetLoopInformation() noexcept; + /** + * @brief Read the loop information data from the region. + * This requires that the region and promise is properly set. + * + */ + void updateLoopInformation() noexcept; FilePromisePtr currentPromise { nullptr }; From cab43b6641462fa24da86e3be9e095670972a8a0 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 5 Oct 2020 14:33:57 +0200 Subject: [PATCH 384/445] Move the crossfade spans closer to their actual use --- src/sfizz/Voice.cpp | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index c1b05e8a..66c7bb1e 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -561,20 +561,6 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // calculate loop characteristics const bool isLooping = region->shouldLoop() && (static_cast(loop.end) < source.getNumFrames()); - SpanHolder> xfadeTemp[2]; - SpanHolder> xfadeIndexTemp[1]; - if (isLooping) { - for (auto& buf : xfadeTemp) { - buf = resources.bufferPool.getBuffer(numSamples); - if (!buf) - return; - } - for (auto& buf : xfadeIndexTemp) { - buf = resources.bufferPool.getIndexBuffer(numSamples); - if (!buf) - return; - } - } /* loop start loop end @@ -693,7 +679,13 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept source, ptBuffer, ptIndices, ptCoeffs, {}, quality); if (ptType == kPartitionLoopXfade) { - absl::Span xfCurvePos = xfadeTemp[0]->first(ptSize); + auto xfTemp1 = resources.bufferPool.getBuffer(numSamples); + auto xfTemp2 = resources.bufferPool.getBuffer(numSamples); + auto xfIndicesTemp = resources.bufferPool.getIndexBuffer(numSamples); + if (!xfTemp1 || !xfTemp2 || !xfIndicesTemp) + return; + + absl::Span xfCurvePos = xfTemp1->first(ptSize); // compute crossfade positions for (unsigned i = 0; i < ptSize; ++i) { @@ -706,7 +698,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // -> fade out signal nearing the loop end { // compute out curve - absl::Span xfCurve = xfadeTemp[1]->first(ptSize); + absl::Span xfCurve = xfTemp2->first(ptSize); IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < ptSize; ++i) @@ -736,7 +728,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // -> fade in signal preceding the loop start { // compute indices of the crossfade input segment - absl::Span xfInIndices = xfadeIndexTemp[0]->first(ptSize); + absl::Span xfInIndices = xfIndicesTemp->first(ptSize); absl::c_copy(ptIndices, xfInIndices.begin()); subtract1(loop.xfOutStart - loop.xfInStart, xfInIndices); @@ -756,7 +748,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); // compute in curve - absl::Span xfCurve = xfadeTemp[1]->first(applySize); + absl::Span xfCurve = xfTemp2->first(applySize); IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < applySize; ++i) From b1a067506ffc3faf74921a046bae33e21e7faf62 Mon Sep 17 00:00:00 2001 From: redtide Date: Tue, 6 Oct 2020 19:54:13 +0200 Subject: [PATCH 385/445] Updated README, added Discord badge link --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bb0e1e6..d55e2f0e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ [![Travis Build Status]](https://travis-ci.com/sfztools/sfizz) [![AppVeyor Build Status]](https://ci.appveyor.com/project/sfztools/sfizz) -SFZ library and LV2 plugin, please check [our website] for more details. +[![Discord Badge Image]](https://discord.gg/3ArE9Mw) + +SFZ parser and synth c++ library, providing AU / LV2 / VST3 plugins +and JACK standalone client, please check [our website] for more details. ![Screenshot](screenshot.png) @@ -64,3 +67,4 @@ The sfizz library also uses in some subprojects: [build from source]: https://sfz.tools/sfizz/development/build/ [AppVeyor Build Status]: https://img.shields.io/appveyor/ci/sfztools/sfizz.svg?label=Windows&style=popout&logo=appveyor [Travis Build Status]: https://img.shields.io/travis/com/sfztools/sfizz.svg?label=Linux&style=popout&logo=travis +[Discord Badge Image]: https://img.shields.io/discord/587748534321807416?label=discord&logo=discord From 24624abef15708d9cd962848fdcdb32bb07c112f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 09:21:49 +0200 Subject: [PATCH 386/445] Move the crossfade curve config to the config --- src/sfizz/Config.h | 4 ++++ src/sfizz/Voice.cpp | 20 +++++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 3c724dad..023e7fed 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -126,6 +126,10 @@ namespace config { @brief Ratio to target under which smoothing is considered as completed */ static constexpr float smoothingShortcutThreshold = 5e-3; + // loop crossfade settings + static constexpr int loopXfadeCurve = 2; // 0: linear + // 1: use curves 5 & 6 + // 2: use S-shaped curve } // namespace config } // namespace sfz diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 66c7bb1e..680f819e 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -588,8 +588,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept partitionStarts = absl::MakeSpan(const_cast(starts), 1); partitionTypes = absl::MakeSpan(const_cast(types), 1); numPartitions = 1; - } - else { + } else { for (auto& buf : partitionBuffers) { buf = resources.bufferPool.getIndexBuffer(numSamples); if (!buf) @@ -601,11 +600,6 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // computed along with index processing below } - // loop crossfade settings - constexpr int loopXfadeUseCurves = 2; // 0: linear - // 1: use curves 5 & 6 - // 2: use S-shaped curve - // index preprocessing for loops if (isLooping) { int oldIndex {}; @@ -699,17 +693,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept { // compute out curve absl::Span xfCurve = xfTemp2->first(ptSize); - IF_CONSTEXPR (loopXfadeUseCurves == 2) { + IF_CONSTEXPR (config::loopXfadeCurve == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = xfIn.evalNormalized(1.0f - xfCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 1) { + else IF_CONSTEXPR (config::loopXfadeCurve == 1) { const Curve& xfOut = resources.curves.getCurve(6); for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = xfOut.evalNormalized(xfCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 0) { + else IF_CONSTEXPR (config::loopXfadeCurve == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = clamp(1.0f - xfCurvePos[i], 0.0f, 1.0f); @@ -749,17 +743,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute in curve absl::Span xfCurve = xfTemp2->first(applySize); - IF_CONSTEXPR (loopXfadeUseCurves == 2) { + IF_CONSTEXPR (config::loopXfadeCurve == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 1) { + else IF_CONSTEXPR (config::loopXfadeCurve == 1) { const Curve& xfIn = resources.curves.getCurve(5); for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 0) { + else IF_CONSTEXPR (config::loopXfadeCurve == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = clamp(xfInCurvePos[i], 0.0f, 1.0f); From 809edfa76cee428a29a59f6683c271d402ad7af0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 21:06:03 +0200 Subject: [PATCH 387/445] Make local copy of the loop characteristics --- src/sfizz/Voice.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 680f819e..1ba67db3 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -559,6 +559,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } // calculate loop characteristics + const auto loop = this->loop; const bool isLooping = region->shouldLoop() && (static_cast(loop.end) < source.getNumFrames()); From 8071cd0131d49e49858cab04d7db541acef80171 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 18:43:09 +0200 Subject: [PATCH 388/445] Add more voices than necessary These will naturally be used for "dying" voices beyond the engine polyphony --- src/sfizz/Config.h | 7 +++++++ src/sfizz/RegionSet.cpp | 5 +++++ src/sfizz/RegionSet.h | 5 +++++ src/sfizz/Synth.cpp | 44 ++++++++++++++++++----------------------- src/sfizz/Synth.h | 6 +++++- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 477c3ce3..eede94f2 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -131,6 +131,13 @@ namespace config { static constexpr int loopXfadeCurve = 2; // 0: linear // 1: use curves 5 & 6 // 2: use S-shaped curve + /** + * @brief Overflow voices in the engine, relative to the required voices. + * These are additional voices that more or less hold the "dying" voices + * due to engine polyphony being reached. + */ + static constexpr float overflowVoiceMultiplier { 1.5f }; + static_assert(overflowVoiceMultiplier >= 1.0f); } // namespace config } // namespace sfz diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp index 1387438a..bfceb4e2 100644 --- a/src/sfizz/RegionSet.cpp +++ b/src/sfizz/RegionSet.cpp @@ -53,3 +53,8 @@ unsigned sfz::RegionSet::numPlayingVoices() const noexcept return !v->releasedOrFree(); }); } + +void sfz::RegionSet::removeAllVoices() noexcept +{ + voices.clear(); +} diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 4424a030..4077fbe7 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -122,6 +122,11 @@ public: * @return const std::vector& */ const std::vector& getSubsets() const noexcept { return subsets; } + + /** + * @brief Remove all voices from the set + */ + void removeAllVoices() noexcept; private: RegionSet* parent { nullptr }; OpcodeScope level { kOpcodeScopeGeneric }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 498a9f62..039b967b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -41,6 +41,7 @@ sfz::Synth::Synth(int numVoices) initializeSIMDDispatchers(); const std::lock_guard disableCallback { callbackGuard }; + engineSet = absl::make_unique(nullptr, OpcodeScope::kOpcodeScopeGeneric); parser.setListener(this); effectFactory.registerStandardEffectTypes(); effectBuses.reserve(5); // sufficient room for main and fx1-4 @@ -387,6 +388,7 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) default: DBG("Unsupported value for hint_stealing: " << member.value); } + break; default: // Unsupported control opcode DBG("Unsupported control opcode: " << member.opcode); @@ -742,23 +744,8 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); - // Engine polyphony reached - Voice* stolenVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); - if (stolenVoice == nullptr) - return {}; - - // Never kill age 0 voices - if (stolenVoice->getAge() == 0) - return {}; - - - auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); - SisterVoiceRing::applyToRing(stolenVoice, [&] (Voice* v) { - renderVoiceToOutputs(*v, *tempSpan); - v->reset(); - }); - - return stolenVoice; + DBG("Engine polyphony reached"); + return {}; } int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept @@ -1511,7 +1498,7 @@ void sfz::Synth::setVolume(float volume) noexcept int sfz::Synth::getNumVoices() const noexcept { - return numVoices; + return numRequiredVoices; } void sfz::Synth::setNumVoices(int numVoices) noexcept @@ -1520,7 +1507,7 @@ void sfz::Synth::setNumVoices(int numVoices) noexcept const std::lock_guard disableCallback { callbackGuard }; // fast path - if (numVoices == this->numVoices) + if (numVoices == this->numRequiredVoices) return; resetVoices(numVoices); @@ -1528,16 +1515,25 @@ void sfz::Synth::setNumVoices(int numVoices) noexcept void sfz::Synth::resetVoices(int numVoices) { + numActualVoices = + static_cast(config::overflowVoiceMultiplier * numVoices); + numRequiredVoices = numVoices; + + for (auto& set : sets) + set->removeAllVoices(); + engineSet->removeAllVoices(); + engineSet->setPolyphonyLimit(numRequiredVoices); + voices.clear(); - voices.reserve(numVoices); + voices.reserve(numActualVoices); voiceViewArray.clear(); - voiceViewArray.reserve(numVoices); + voiceViewArray.reserve(numActualVoices); tempPolyphonyArray.clear(); - tempPolyphonyArray.reserve(numVoices); + tempPolyphonyArray.reserve(numActualVoices); - for (int i = 0; i < numVoices; ++i) { + for (int i = 0; i < numActualVoices; ++i) { auto voice = absl::make_unique(i, resources); voice->setStateListener(this); voiceViewArray.push_back(voice.get()); @@ -1549,8 +1545,6 @@ void sfz::Synth::resetVoices(int numVoices) voice->setSamplesPerBlock(this->samplesPerBlock); } - this->numVoices = numVoices; - applySettingsPerVoice(); } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index c0d32c2e..2663afcc 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -819,6 +819,9 @@ private: // These are more general "groups" than sfz and encapsulates the full hierarchy RegionSet* currentSet { nullptr }; std::vector sets; + // This region set holds the engine set of voices, which tries to respect the required + // engine polyphony + RegionSetPtr engineSet; // These are the `group=` groups where you can off voices std::vector polyphonyGroups; @@ -902,7 +905,8 @@ private: int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; float volume { Default::globalVolume }; - int numVoices { config::numVoices }; + int numRequiredVoices { config::numVoices }; + int numActualVoices { static_cast(config::numVoices * config::overflowVoiceMultiplier) }; int activeVoices { 0 }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; From 480a7d4628cc1087b21dbac19dd60379dda1d435 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 21:49:28 +0200 Subject: [PATCH 389/445] Use the soft engine polyphony limit --- src/sfizz/SisterVoiceRing.h | 5 +++-- src/sfizz/Synth.cpp | 18 +++++++++++++++++- src/sfizz/Synth.h | 7 +++++++ src/sfizz/Voice.cpp | 6 +++--- src/sfizz/Voice.h | 3 ++- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 4e7cc743..bb6137bb 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -61,13 +61,14 @@ struct SisterVoiceRing { * * @param voice * @param delay + * @param fast whether to apply a fast release */ template>::value, int> = 0> - static void offAllSisters(T* voice, int delay) { + static void offAllSisters(T* voice, int delay, bool fast = false) { if (voice != nullptr) { SisterVoiceRing::applyToRing(voice, [&] (Voice* v) { - v->off(delay); + v->off(delay, fast); }); } } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 039b967b..245a7240 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -71,6 +71,7 @@ void sfz::Synth::onVoiceStateChanged(NumericId id, Voice::State state) if (state == Voice::State::idle) { auto voice = getVoiceById(id); RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); + engineSet->removeVoice(voice); polyphonyGroups[voice->getRegion()->group].removeVoice(voice); } @@ -744,7 +745,7 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); - DBG("Engine polyphony reached"); + DBG("Engine hard polyphony reached"); return {}; } @@ -972,6 +973,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg checkRegionPolyphony(region, delay); checkGroupPolyphony(region, delay); checkSetPolyphony(region, delay); + checkEnginePolyphony(delay); Voice* selectedVoice = findFreeVoice(); if (selectedVoice == nullptr) @@ -980,6 +982,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg ASSERT(selectedVoice->isFree()); selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); + engineSet->registerVoice(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); } @@ -1108,6 +1111,19 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept } } +void sfz::Synth::checkEnginePolyphony(int delay) noexcept +{ + auto& activeVoices = engineSet->getActiveVoices(); + + if (activeVoices.size() >= static_cast(numRequiredVoices)) { + tempPolyphonyArray.clear(); + absl::c_copy_if(activeVoices, + std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); }); + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay, true); + } +} + void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 2663afcc..af3aa469 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -865,6 +865,13 @@ private: */ void checkSetPolyphony(const Region* region, int delay) noexcept; + /** + * @brief Check the engine polyphony, fast releasing voices if necessary + * + * @param delay + */ + void checkEnginePolyphony(int delay) noexcept; + /** * @brief Start a voice for a specific region. * This will do the needed polyphony checks and voice stealing. diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 7dd2f948..96eb9ada 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -167,11 +167,11 @@ void sfz::Voice::release(int delay) noexcept resources.modMatrix.releaseVoice(id, region->getId(), delay); } -void sfz::Voice::off(int delay) noexcept +void sfz::Voice::off(int delay, bool fast) noexcept { if (!region->flexAmpEG) { - if (region->offMode == SfzOffMode::fast) { - egAmplitude.setReleaseTime( Default::offTime ); + if (region->offMode == SfzOffMode::fast || fast) { + egAmplitude.setReleaseTime(Default::offTime); } else if (region->offMode == SfzOffMode::time) { egAmplitude.setReleaseTime(region->offTime); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index dbcae7d7..54fd16e5 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -328,8 +328,9 @@ public: * and set the envelopes if necessary. * * @param delay + * @param fast whether to apply a fast release regardless of the off mode */ - void off(int delay) noexcept; + void off(int delay, bool fast = false) noexcept; /** * @brief gets the age of the Voice From 02a1615e2d131b26456fef704e4da73e00a3f8d7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 23:03:25 +0200 Subject: [PATCH 390/445] Use the back inserter idiom in all stealing methods --- src/sfizz/Synth.cpp | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 245a7240..2348dcb4 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1024,12 +1024,9 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept { tempPolyphonyArray.clear(); - - for (Voice* voice : voiceViewArray) { - if (voice->getRegion() == region && !voice->releasedOrFree()) { - tempPolyphonyArray.push_back(voice); - } - } + absl::c_copy_if(voiceViewArray, + std::back_inserter(tempPolyphonyArray), + [region](Voice* v) { return v->getRegion() == region && !v->releasedOrFree(); }); if (tempPolyphonyArray.size() >= region->polyphony) { const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); @@ -1078,11 +1075,8 @@ void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept { const auto& activeVoices = polyphonyGroups[region->group].getActiveVoices(); tempPolyphonyArray.clear(); - for (Voice* voice : activeVoices) { - if (!voice->releasedOrFree()) { - tempPolyphonyArray.push_back(voice); - } - } + absl::c_copy_if(activeVoices, + std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); }); if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) { const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); @@ -1096,11 +1090,8 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept while (parent != nullptr) { const auto& activeVoices = parent->getActiveVoices(); tempPolyphonyArray.clear(); - for (Voice* voice : activeVoices) { - if (!voice->releasedOrFree()) { - tempPolyphonyArray.push_back(voice); - } - } + absl::c_copy_if(activeVoices, + std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); }); if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) { const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); From 8212eb4966fbbe24a4a7fcecea8d3f95b663f4bd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 23:58:27 +0200 Subject: [PATCH 391/445] cpp11 static assert needs a message --- src/sfizz/Config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index eede94f2..378f4476 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -137,7 +137,7 @@ namespace config { * due to engine polyphony being reached. */ static constexpr float overflowVoiceMultiplier { 1.5f }; - static_assert(overflowVoiceMultiplier >= 1.0f); + static_assert(overflowVoiceMultiplier >= 1.0f, "This needs to add voices"); } // namespace config } // namespace sfz From 8ee4105b399e343f5c39233681f5465773534559 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 18 Sep 2020 17:12:12 +0200 Subject: [PATCH 392/445] Deep change of the background loadingThe goal was to reduce the number of background threads in big sessions (multiple instances) and avoid loading the same file alot on repeated notes (e.g. drum tracks).- All instances use a common thread pool- Each instance has a dispatching job that starts background loader, and a garbage job that clears data that has been unused for some time- The "FilePromise" object has disappeared. Upon request, Voices get a very thin reference-counting handler to the file data. When the handler is released the reference count is decreased and a "last used" timestamp is added to the file.- The background file loading job checks i) if the file is already loaded, or ii) if some other thread is already loading it. If any case is validated, it exits assuming the background loading is already happening.- The garbage job has to be triggered regularly by e.g. the synth. In the RT thread the triggerGarbageCollection() method will check the list of previously loaded file Ids to see if any has not been used for a while. If so, the memory is set to be discarded by a background thread, otherwise it'll wait for the next ping.Only tested on Linux for now. --- src/external/threadpool/ThreadPool.h | 100 ++++++++ src/sfizz/Config.h | 1 + src/sfizz/FilePool.cpp | 371 +++++++++++++++------------ src/sfizz/FilePool.h | 192 +++++++++----- src/sfizz/Synth.cpp | 10 +- src/sfizz/Synth.h | 2 + src/sfizz/Voice.cpp | 27 +- src/sfizz/Voice.h | 2 +- src/sfizz/Wavetables.cpp | 4 +- 9 files changed, 450 insertions(+), 259 deletions(-) create mode 100644 src/external/threadpool/ThreadPool.h diff --git a/src/external/threadpool/ThreadPool.h b/src/external/threadpool/ThreadPool.h new file mode 100644 index 00000000..36169849 --- /dev/null +++ b/src/external/threadpool/ThreadPool.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: zlib + +#ifndef THREAD_POOL_H +#define THREAD_POOL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ThreadPool { +public: + ThreadPool(size_t); + template + auto enqueue(F&& f, Args&&... args) + -> std::future::type>; + ~ThreadPool(); +private: + // need to keep track of threads so we can join them + std::vector< std::thread > workers; + // the task queue + std::queue< std::function > tasks; + + // synchronization + std::mutex queue_mutex; + std::condition_variable condition; + bool stop; +}; + +// the constructor just launches some amount of workers +inline ThreadPool::ThreadPool(size_t threads) + : stop(false) +{ + for(size_t i = 0;i task; + + { + std::unique_lock lock(this->queue_mutex); + this->condition.wait(lock, + [this]{ return this->stop || !this->tasks.empty(); }); + if(this->stop && this->tasks.empty()) + return; + task = std::move(this->tasks.front()); + this->tasks.pop(); + } + + task(); + } + } + ); +} + +// add new work item to the pool +template +auto ThreadPool::enqueue(F&& f, Args&&... args) + -> std::future::type> +{ + using return_type = typename std::result_of::type; + + auto task = std::make_shared< std::packaged_task >( + std::bind(std::forward(f), std::forward(args)...) + ); + + std::future res = task->get_future(); + { + std::unique_lock lock(queue_mutex); + + // don't allow enqueueing after stopping the pool + if(stop) + throw std::runtime_error("enqueue on stopped ThreadPool"); + + tasks.emplace([task](){ (*task)(); }); + } + condition.notify_one(); + return res; +} + +// the destructor joins all threads +inline ThreadPool::~ThreadPool() +{ + { + std::unique_lock lock(queue_mutex); + stop = true; + } + condition.notify_all(); + for(std::thread &worker: workers) + worker.join(); +} + +#endif diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 477c3ce3..1c8b97a7 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -39,6 +39,7 @@ namespace config { constexpr bool loggingEnabled { false }; constexpr size_t numChannels { 2 }; constexpr int numBackgroundThreads { 4 }; + constexpr unsigned fileClearingPeriod { 5 }; // in seconds constexpr int numVoices { 64 }; constexpr unsigned maxVoices { 256 }; constexpr unsigned smoothingSteps { 512 }; diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index c92ebcbd..ec9dee51 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -45,6 +45,9 @@ #else #include #endif +#include "threadpool/ThreadPool.h" +using namespace std::placeholders; +static ThreadPool threadPool { sfz::config::numBackgroundThreads }; void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32_t numFrames) { @@ -67,18 +70,18 @@ void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32 } } -std::unique_ptr readFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor) +sfz::FileAudioBuffer readFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor) { - auto baseBuffer = absl::make_unique(); - readBaseFile(reader, *baseBuffer, numFrames); + sfz::FileAudioBuffer baseBuffer; + readBaseFile(reader, baseBuffer, numFrames); if (factor == sfz::Oversampling::x1) return baseBuffer; - auto outputBuffer = absl::make_unique(reader.channels(), numFrames * static_cast(factor)); - outputBuffer->clear(); + sfz::FileAudioBuffer outputBuffer { reader.channels(), numFrames * static_cast(factor) }; + outputBuffer.clear(); sfz::Oversampler oversampler { factor }; - oversampler.stream(*baseBuffer, *outputBuffer); + oversampler.stream(baseBuffer, outputBuffer); return outputBuffer; } @@ -93,37 +96,27 @@ void streamFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampl } sfz::FilePool::FilePool(sfz::Logger& logger) -: logger(logger) + : logger(logger) { - FilePromise promise; - if (!promise.dataStatus.is_lock_free()) - DBG("atomic is not lock-free; could cause issues with locking"); - - for (int i = 0; i < config::numBackgroundThreads; ++i) - threadPool.emplace_back( &FilePool::loadingThread, this ); - - threadPool.emplace_back( &FilePool::clearingThread, this ); - - for (int i = 0; i < config::maxFilePromises; ++i) - emptyPromises.push_back(std::make_shared()); + loadingJobs.reserve(config::maxVoices); + loadingJobs.reserve(config::maxVoices); + garbageToCollect.reserve(config::maxVoices); } sfz::FilePool::~FilePool() { - quitThread = true; - std::error_code ec; - for (unsigned i = 0; i < threadPool.size(); ++i) { - ec = std::error_code(); - workerBarrier.post(ec); - } + garbageFlag = false; + semGarbageBarrier.post(ec); + garbageThread.join(); - ec = std::error_code(); - semClearingRequest.post(ec); + dispatchFlag = false; + dispatchBarrier.post(ec); + dispatchThread.join(); - for (auto& thread: threadPool) - thread.join(); + for (auto& job : loadingJobs) + job.wait(); } bool sfz::FilePool::checkSample(std::string& filename) const noexcept @@ -142,7 +135,7 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept static const fs::path dot { "." }; static const fs::path dotdot { ".." }; - for (const fs::path &part : oldPath.relative_path()) { + for (const fs::path& part : oldPath.relative_path()) { if (part == dot || part == dotdot) { path /= part; continue; @@ -153,7 +146,7 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept continue; } - auto it = path.empty() ? fs::directory_iterator{ dot, ec } : fs::directory_iterator{ path, ec }; + auto it = path.empty() ? fs::directory_iterator { dot, ec } : fs::directory_iterator { path, ec }; if (ec) { DBG("Error creating a directory iterator for " << filename << " (Error code: " << ec.message() << ")"); return false; @@ -169,10 +162,10 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept #endif }; - while (it != fs::directory_iterator{} && !searchPredicate(*it)) + while (it != fs::directory_iterator {} && !searchPredicate(*it)) it.increment(ec); - if (it == fs::directory_iterator{}) { + if (it == fs::directory_iterator {}) { DBG("File not found, could not resolve " << filename); return false; } @@ -275,22 +268,30 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce const auto existingFile = preloadedFiles.find(fileId); if (existingFile != preloadedFiles.end()) { - if (framesToLoad > existingFile->second.preloadedData->getNumFrames()) { + if (framesToLoad > existingFile->second.preloadedData.getNumFrames()) { preloadedFiles[fileId].information.maxOffset = maxOffset; preloadedFiles[fileId].preloadedData = readFromFile(*reader, framesToLoad, oversamplingFactor); } } else { - fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(reader->sampleRate()); - FileDataHandle handle { + const auto factor = static_cast(oversamplingFactor); + fileInformation->sampleRate = factor * static_cast(reader->sampleRate()); + fileInformation->end = static_cast(factor * fileInformation->end); + fileInformation->loopBegin = static_cast(factor * fileInformation->loopBegin); + fileInformation->loopEnd = static_cast(factor * fileInformation->loopEnd); + auto insertedPair = preloadedFiles.insert_or_assign(fileId, { readFromFile(*reader, framesToLoad, oversamplingFactor), *fileInformation - }; - preloadedFiles.insert_or_assign(fileId, handle); + }); + + if (!insertedPair.second) + return false; + + insertedPair.first->second.status = FileData::Status::Preloaded; } return true; } -absl::optional sfz::FilePool::loadFile(const FileId& fileId) noexcept +sfz::FileDataHolder sfz::FilePool::loadFile(const FileId& fileId) noexcept { auto fileInformation = getFileInformation(fileId); if (!fileInformation) @@ -299,54 +300,44 @@ absl::optional sfz::FilePool::loadFile(const FileId& fileId const fs::path file { rootDirectory / fileId.filename() }; AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); - // FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same const auto frames = static_cast(reader->frames()); const auto existingFile = loadedFiles.find(fileId); if (existingFile != loadedFiles.end()) { - return existingFile->second; + return { &existingFile->second }; } else { - fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(reader->sampleRate()); - FileDataHandle handle { + const auto factor = static_cast(oversamplingFactor); + fileInformation->sampleRate = factor * static_cast(reader->sampleRate()); + fileInformation->end = static_cast(factor * fileInformation->end); + fileInformation->loopBegin = static_cast(factor * fileInformation->loopBegin); + fileInformation->loopEnd = static_cast(factor * fileInformation->loopEnd); + auto insertedPair = preloadedFiles.insert_or_assign(fileId, { readFromFile(*reader, frames, oversamplingFactor), *fileInformation - }; - loadedFiles.insert_or_assign(fileId, handle); - return handle; + }); + insertedPair.first->second.status = FileData::Status::Preloaded; + ASSERT(insertedPair.second); + return { &insertedPair.first->second }; } } -sfz::FilePromisePtr sfz::FilePool::getFilePromise(const FileId& fileId) noexcept +sfz::FileDataHolder sfz::FilePool::getFilePromise(const FileId& fileId) noexcept { - if (emptyPromises.empty()) { - DBG("[sfizz] No empty promises left to honor the one for " << fileId); - return {}; - } - const auto preloaded = preloadedFiles.find(fileId); if (preloaded == preloadedFiles.end()) { DBG("[sfizz] File not found in the preloaded files: " << fileId); return {}; } - - auto promise = emptyPromises.back(); - promise->fileId = preloaded->first; - promise->preloadedData = preloaded->second.preloadedData; - promise->sampleRate = static_cast(preloaded->second.information.sampleRate); - promise->oversamplingFactor = oversamplingFactor; - promise->creationTime = std::chrono::high_resolution_clock::now(); - - if (!promiseQueue.try_push(promise)) { - DBG("[sfizz] Could not enqueue the promise for " << fileId << " (queue capacity " << promiseQueue.capacity() << ")"); + QueuedFileData queuedData { fileId, &preloaded->second, std::chrono::high_resolution_clock::now() }; + if (!filesToLoad.try_push(queuedData)) { + DBG("[sfizz] Could not enqueue the file to load for " << fileId << " (queue capacity " << filesToLoad.capacity() << ")"); return {}; } std::error_code ec; - workerBarrier.post(ec); + dispatchBarrier.post(ec); ASSERT(!ec); - emptyPromises.pop_back(); - - return promise; + return { &preloaded->second }; } void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept @@ -358,118 +349,67 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept // Update all the preloaded sizes for (auto& preloadedFile : preloadedFiles) { const auto maxOffset = preloadedFile.second.information.maxOffset; + const auto numFrames = preloadedFile.second.preloadedData.getNumFrames() / static_cast(oversamplingFactor); fs::path file { rootDirectory / preloadedFile.first.filename() }; AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, oversamplingFactor); } } -void sfz::FilePool::tryToClearPromises() +void sfz::FilePool::loadingJob(QueuedFileData data) noexcept { - const std::lock_guard promiseLock { promiseGuard }; + raiseCurrentThreadPriority(); - for (auto& promise: promisesToClear) { - if (promise->dataStatus != FilePromise::DataStatus::Wait) - promise->reset(); + const auto loadStartTime = std::chrono::high_resolution_clock::now(); + const auto waitDuration = loadStartTime - data.queuedTime; + const fs::path file { rootDirectory / data.id.filename() }; + std::error_code readError; + AudioReaderPtr reader = createAudioReader(file, data.id.isReverse(), &readError); + + if (readError) { + DBG("[sfizz] libsndfile errored for " << data.id << " with message " << readError.message()); + return; } -} -void sfz::FilePool::clearingThread() -{ - raiseCurrentThreadPriority(); + FileData::Status currentStatus = data.data->status.load(); - RTSemaphore& request = semClearingRequest; - do { - request.wait(); - if (quitThread) + unsigned spinCounter { 0 }; + if (currentStatus == FileData::Status::Invalid) { + // Spin until the state changes + if (spinCounter > 1024) { + DBG("[sfizz] " << data.id << " is stuck on Invalid? Leaving the load"); return; - tryToClearPromises(); - } while (1); -} - -void sfz::FilePool::loadingThread() noexcept -{ - raiseCurrentThreadPriority(); - - FilePromisePtr promise; - do { - workerBarrier.wait(); - - if (emptyQueue) { - while (promiseQueue.try_pop(promise)) { - // We're just dequeuing - } - emptyQueue = false; - semEmptyQueueFinished.post(); - continue; } - if (quitThread) - return; + std::this_thread::sleep_for(std::chrono::microseconds(100)); + currentStatus = data.data->status.load(); + spinCounter += 1; + } - if (!promiseQueue.try_pop(promise)) { - continue; - } + // Already loading or loaded + if (currentStatus != FileData::Status::Preloaded) + return; - threadsLoading++; - const auto loadStartTime = std::chrono::high_resolution_clock::now(); - const auto waitDuration = loadStartTime - promise->creationTime; + // Someone else got the token + if (!data.data->status.compare_exchange_strong(currentStatus, FileData::Status::Streaming)) + return; - const fs::path file { rootDirectory / promise->fileId.filename() }; - std::error_code readError; - AudioReaderPtr reader = createAudioReader(file, promise->fileId.isReverse(), &readError); - if (readError) { - DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << readError.message()); - promise->dataStatus = FilePromise::DataStatus::Error; - continue; - } - const auto frames = static_cast(reader->frames()); - streamFromFile(*reader, frames, oversamplingFactor, promise->fileData, &promise->availableFrames); - promise->dataStatus = FilePromise::DataStatus::Ready; - const auto loadDuration = std::chrono::high_resolution_clock::now() - loadStartTime; - logger.logFileTime(waitDuration, loadDuration, frames, promise->fileId.filename()); + const auto frames = static_cast(reader->frames()); + streamFromFile(*reader, frames, oversamplingFactor, data.data->fileData, &data.data->availableFrames); + const auto loadDuration = std::chrono::high_resolution_clock::now() - loadStartTime; + logger.logFileTime(waitDuration, loadDuration, frames, data.id.filename()); - threadsLoading--; + data.data->status = FileData::Status::Done; - semFilledPromiseQueueAvailable.wait(); - filledPromiseQueue.push(promise); - - promise.reset(); - } while (1); + std::lock_guard guard { lastUsedMutex }; + if (absl::c_find(lastUsedFiles, data.id) == lastUsedFiles.end()) + lastUsedFiles.push_back(data.id); } void sfz::FilePool::clear() { emptyFileLoadingQueues(); preloadedFiles.clear(); - temporaryFilePromises.clear(); - promisesToClear.clear(); -} - -void sfz::FilePool::cleanupPromises() noexcept -{ - const std::unique_lock lock { promiseGuard, std::try_to_lock }; - if (!lock.owns_lock()) - return; - - // The garbage collection cleared the data from these so we can move them - // back to the empty queue - auto promiseWaiting = [](FilePromisePtr& p) { return p->waiting(); }; - auto moveToEmpty = [&](FilePromisePtr& p) { return emptyPromises.push_back(p); }; - swapAndPopAll(promisesToClear, promiseWaiting, moveToEmpty); - - // Remove the promises from the filled queue and put them in a linear - // storage - FilePromisePtr promise; - while (filledPromiseQueue.try_pop(promise)) { - semFilledPromiseQueueAvailable.post(); - temporaryFilePromises.push_back(promise); - } - - auto promiseUsedOnce = [](FilePromisePtr& p) { return p.use_count() == 1; }; - auto moveToClear = [&](FilePromisePtr& p) { return promisesToClear.push_back(p); }; - if (swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear) > 0) - semClearingRequest.post(); } void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept @@ -485,10 +425,22 @@ void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept preloadedFile.second.information.maxOffset + preloadSize ); }(); + fs::path file { rootDirectory / preloadedFile.first.filename() }; AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); preloadedFile.second.preloadedData = readFromFile(*reader, framesToLoad, factor); - preloadedFile.second.information.sampleRate *= samplerateChange; + FileInformation& information = preloadedFile.second.information; + information.sampleRate *= samplerateChange; + information.end = static_cast(samplerateChange * information.end); + information.loopBegin = static_cast(samplerateChange * information.loopBegin); + information.loopEnd = static_cast(samplerateChange * information.loopEnd); + + if (preloadedFile.second.status == FileData::Status::Done) { + const auto realFrames = + preloadedFile.second.availableFrames.load() / static_cast(this->oversamplingFactor); + preloadedFile.second.fileData = readFromFile(*reader, realFrames, factor); + preloadedFile.second.availableFrames = realFrames * static_cast(factor); + } } this->oversamplingFactor = factor; @@ -504,26 +456,71 @@ uint32_t sfz::FilePool::getPreloadSize() const noexcept return preloadSize; } +template +bool is_ready(std::future const& f) +{ + return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready; +} + +void sfz::FilePool::dispatchingJob() noexcept +{ + QueuedFileData queuedData; + while (dispatchFlag) { + dispatchBarrier.wait(); + + if (emptyQueueFlag) { + while (filesToLoad.try_pop(queuedData)) { + // pass + } + semEmptyQueueFinished.post(); + emptyQueueFlag = false; + continue; + } + + if (filesToLoad.try_pop(queuedData)) { + loadingJobs.push_back( + threadPool.enqueue([this](const QueuedFileData& data) { loadingJob(data); }, queuedData)); + } + + // Clear finished jobs + swapAndPopAll(loadingJobs, [](std::future& future) { + return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; + }); + } +} + +void sfz::FilePool::garbageJob() noexcept +{ + std::chrono::seconds counter { 0 }; // This avoids waiting to long on the thread + constexpr std::chrono::milliseconds timeAtom { 100 }; + while (garbageFlag) { + semGarbageBarrier.wait(); + { + std::lock_guard guard { garbageMutex }; + for (auto& g: garbageToCollect) + g.reset(); + + garbageToCollect.clear(); + } + } +} + void sfz::FilePool::emptyFileLoadingQueues() noexcept { - emptyQueue = true; - workerBarrier.post(); - semEmptyQueueFinished.wait(); + ASSERT(dispatchFlag); + emptyQueueFlag = true; + std::error_code ec; + dispatchBarrier.post(ec); + + if (!ec) + semEmptyQueueFinished.wait(); } void sfz::FilePool::waitForBackgroundLoading() noexcept { - // TODO: validate that this is enough, otherwise we will need an atomic count - // of the files we need to load still. - // Spinlocking on the size of the background queue - while (!promiseQueue.was_empty()){ - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } - - // Spinlocking on the threads possibly logging in the background - while (threadsLoading > 0) { - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } + for (auto& job : loadingJobs) + job.wait(); + loadingJobs.clear(); } void sfz::FilePool::raiseCurrentThreadPriority() noexcept @@ -548,8 +545,7 @@ void sfz::FilePool::raiseCurrentThreadPriority() noexcept policy = SCHED_RR; const int minprio = sched_get_priority_min(policy); const int maxprio = sched_get_priority_max(policy); - param.sched_priority = minprio + - config::backgroundLoaderPthreadPriority * (maxprio - minprio) / 100; + param.sched_priority = minprio + config::backgroundLoaderPthreadPriority * (maxprio - minprio) / 100; if (pthread_setschedparam(thread, policy, ¶m) != 0) { DBG("[sfizz] Cannot set current thread scheduling parameters"); @@ -579,3 +575,40 @@ void sfz::FilePool::setRamLoading(bool loadInRam) noexcept setPreloadSize(preloadSize); } } + +void sfz::FilePool::triggerGarbageCollection() noexcept +{ + const std::unique_lock lastUsedLock { lastUsedMutex, std::try_to_lock }; + const std::unique_lock garbageLock { garbageMutex, std::try_to_lock }; + if (!lastUsedLock.owns_lock() || !garbageLock.owns_lock()) + return; + + const auto now = std::chrono::high_resolution_clock::now(); + swapAndPopAll(lastUsedFiles, [&](const FileId& id) { + if (garbageToCollect.size() == garbageToCollect.capacity()) + return false; + + auto& data = preloadedFiles[id]; + if (data.status == FileData::Status::Preloaded) + return true; + + if (data.status != FileData::Status::Done) + return false; + + if (data.readerCount != 0) + return false; + + const auto secondsIdle = std::chrono::duration_cast(now - data.lastViewerLeftAt).count(); + if (secondsIdle < config::fileClearingPeriod) + return false; + + data.availableFrames = 0; + data.status = FileData::Status::Preloaded; + garbageToCollect.push_back(std::move(data.fileData)); + return true; + }); + + std::error_code ec; + semGarbageBarrier.post(ec); + ASSERT(!ec); +} diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index ad357ff6..53846ad1 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -42,7 +42,8 @@ #include "Logger.h" #include #include -#include +#include +#include "utility/SpinMutex.h" namespace sfz { using FileAudioBuffer = AudioBuffer getData() { - if (dataStatus == DataStatus::Ready) - return AudioSpan(fileData); - else if (availableFrames > preloadedData->getNumFrames()) + if (availableFrames > preloadedData.getNumFrames()) return AudioSpan(fileData).first(availableFrames); else - return AudioSpan(*preloadedData); + return AudioSpan(preloadedData); } - void reset() + FileData(const FileData& other) = default; + FileData& operator=(const FileData& other) = default; + FileData(FileData&& other) { - fileData.reset(); - preloadedData.reset(); - fileId = FileId {}; - availableFrames = 0; - dataStatus = DataStatus::Wait; - oversamplingFactor = config::defaultOversamplingFactor; - sampleRate = config::defaultSampleRate; + ASSERT(other.readerCount == 0); // Probably should not be moving this... + information = std::move(other.information); + preloadedData = std::move(other.preloadedData); + fileData = std::move(other.fileData); + availableFrames = other.availableFrames.load(); + lastViewerLeftAt = other.lastViewerLeftAt; + status = other.status.load(); } - - enum class DataStatus { - Wait = 0, - Ready, - Error, - }; - - bool waiting() const { return dataStatus == DataStatus::Wait; } - - void sleepUntilComplete() + FileData& operator=(FileData&& other) { - while (waiting()) - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + ASSERT(other.readerCount == 0); // Probably should not be moving this... + information = std::move(other.information); + preloadedData = std::move(other.preloadedData); + fileData = std::move(other.fileData); + availableFrames = other.availableFrames.load(); + lastViewerLeftAt = other.lastViewerLeftAt; + status = other.status.load(); + return *this; } - FileId fileId {}; - FileAudioBufferPtr preloadedData {}; + FileAudioBuffer preloadedData; + FileInformation information; FileAudioBuffer fileData {}; - float sampleRate { config::defaultSampleRate }; - Oversampling oversamplingFactor { config::defaultOversamplingFactor }; + std::atomic status { Status::Invalid }; std::atomic availableFrames { 0 }; - std::atomic dataStatus { DataStatus::Wait }; - std::chrono::time_point creationTime; + std::atomic readerCount { 0 }; + std::chrono::time_point lastViewerLeftAt; - LEAK_DETECTOR(FilePromise); + LEAK_DETECTOR(FileData); +}; + + +class FileDataHolder { +public: + FileDataHolder() = default; + FileDataHolder(const FileDataHolder&) = delete; + FileDataHolder& operator=(const FileDataHolder&) = delete; + FileDataHolder(FileDataHolder&& other) + { + this->data = other.data; + other.data = nullptr; + } + FileDataHolder& operator=(FileDataHolder&& other) + { + this->data = other.data; + other.data = nullptr; + return *this; + } + FileDataHolder(FileData* data) : data(data) + { + if (!data) + return; + + data->readerCount += 1; + } + void reset() + { + if (!data) + return; + + data->readerCount -= 1; + data->lastViewerLeftAt = std::chrono::high_resolution_clock::now(); + data = nullptr; + } + ~FileDataHolder() + { + ASSERT(!data || data->readerCount > 0); + reset(); + } + FileData& operator*() { return *data; } + FileData* operator->() { return data; } + explicit operator bool() const { return data != nullptr; } +private: + FileData* data { nullptr }; + LEAK_DETECTOR(FileDataHolder); }; -using FilePromisePtr = std::shared_ptr; /** * @brief This is a singleton-designed class that holds all the preloaded data * as well as functions to request new file data and collect the file handles to @@ -188,7 +231,7 @@ public: * @param fileId * @return A handle on the file data */ - absl::optional loadFile(const FileId& fileId) noexcept; + FileDataHolder loadFile(const FileId& fileId) noexcept; /** * @brief Check that the sample exists. If not, try to find it in a case insensitive way. @@ -214,19 +257,12 @@ public: */ void clear(); /** - * @brief Moves the filled promises to a linear storage, and checks - * said linear storage for promises that are not used anymore. - * - * This function has to be called on the audio thread. - */ - void cleanupPromises() noexcept; - /** - * @brief Get a file promise + * @brief Get a handle on a file, which triggers background loading * * @param fileId the file to preload - * @return FilePromisePtr a file promise + * @return FileDataHolder a file data handle */ - FilePromisePtr getFilePromise(const FileId& fileId) noexcept; + FileDataHolder getFilePromise(const FileId& fileId) noexcept; /** * @brief Change the preloading size. This will trigger a full * reload of all samples, so don't call it on the audio thread. @@ -277,37 +313,51 @@ public: * @param loadInRam */ void setRamLoading(bool loadInRam) noexcept; + /** + * @brief Prepares unused data to be freed on a background thread. + * This should be called regularly by the Synth, otherwise memory + * risk building up. + */ + void triggerGarbageCollection() noexcept; private: Logger& logger; fs::path rootDirectory; - void loadingThread() noexcept; - void clearingThread(); - void tryToClearPromises(); - atomic_queue::AtomicQueue2 promiseQueue; - atomic_queue::AtomicQueue2 filledPromiseQueue; - RTSemaphore semFilledPromiseQueueAvailable { config::maxVoices }; bool loadInRam { config::loadInRam }; uint32_t preloadSize { config::preloadSize }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; - // Signals - volatile bool quitThread { false }; - volatile bool emptyQueue { false }; - RTSemaphore semEmptyQueueFinished; - std::atomic threadsLoading { 0 }; - RTSemaphore workerBarrier; - RTSemaphore semClearingRequest; - // File promises data structures along with their guards. - std::vector emptyPromises; - std::vector temporaryFilePromises; - std::vector promisesToClear; - SpinMutex promiseGuard; + // Signals + volatile bool dispatchFlag { true }; + volatile bool garbageFlag { true }; + volatile bool emptyQueueFlag { false }; + RTSemaphore dispatchBarrier; + RTSemaphore semEmptyQueueFinished; + RTSemaphore semGarbageBarrier; + + // Structures for the background loaders + struct QueuedFileData + { + FileId id; + FileData* data; + std::chrono::time_point queuedTime; + }; + atomic_queue::AtomicQueue2 filesToLoad; + void dispatchingJob() noexcept; + void garbageJob() noexcept; + void loadingJob(QueuedFileData data) noexcept; + std::vector> loadingJobs; + std::thread dispatchThread { &FilePool::dispatchingJob, this }; + std::thread garbageThread { &FilePool::garbageJob, this }; + + SpinMutex lastUsedMutex; + std::vector lastUsedFiles; + SpinMutex garbageMutex; + std::vector garbageToCollect; // Preloaded data - absl::flat_hash_map preloadedFiles; - absl::flat_hash_map loadedFiles; - std::vector threadPool { }; + absl::flat_hash_map preloadedFiles; + absl::flat_hash_map loadedFiles; LEAK_DETECTOR(FilePool); }; } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 498a9f62..1a2f7183 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -840,6 +840,15 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (resources.synthConfig.freeWheeling) resources.filePool.waitForBackgroundLoading(); + const auto now = std::chrono::high_resolution_clock::now(); + const auto timeSinceLastCollection = + std::chrono::duration_cast(now - lastGarbageCollection); + + if (timeSinceLastCollection.count() > config::fileClearingPeriod) { + lastGarbageCollection = now; + resources.filePool.triggerGarbageCollection(); + } + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -860,7 +869,6 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; tempMixSpan->fill(0.0f); - resources.filePool.cleanupPromises(); // Ramp out whatever is in the buffer at this point; should only be killed voice data linearRamp(*rampSpan, 1.0f, -1.0f / static_cast(numFrames)); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index c0d32c2e..bea54281 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -941,6 +941,8 @@ private: Duration dispatchDuration { 0 }; + std::chrono::time_point lastGarbageCollection; + Parser parser; fs::file_time_type modificationTime { }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 7dd2f948..92092404 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -95,13 +95,13 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event setupOscillatorUnison(); } else { currentPromise = resources.filePool.getFilePromise(region->sampleId); - if (currentPromise == nullptr) { + if (!currentPromise) { switchState(State::cleanMeUp); return; } updateLoopInformation(); - speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); - sourcePosition = region->getOffset(currentPromise->oversamplingFactor); + speedRatio = static_cast(currentPromise->information.sampleRate / this->sampleRate); + sourcePosition = region->getOffset(resources.filePool.getOversamplingFactor()); } // do Scala retuning and reconvert the frequency into a 12TET key number @@ -545,7 +545,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept if (numSamples == 0) return; - if (currentPromise == nullptr) { + if (!currentPromise) { DBG("[Voice] Missing promise during fillWithData"); return; } @@ -647,17 +647,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept else { // cut short the voice at the instant of reaching end of sample const auto sampleEnd = min( - static_cast(region->trueSampleEnd(currentPromise->oversamplingFactor)), + static_cast(currentPromise->information.end), static_cast(source.getNumFrames()) ) - 1; for (unsigned i = 0; i < numSamples; ++i) { if ((*indices)[i] >= sampleEnd) { #ifndef NDEBUG // Check for underflow - if (source.getNumFrames() - 1 < region->trueSampleEnd(currentPromise->oversamplingFactor)) { + if (source.getNumFrames() - 1 < currentPromise->information.end) { DBG("[sfizz] Underflow: source available samples " << source.getNumFrames() << "/" - << region->trueSampleEnd(currentPromise->oversamplingFactor) + << currentPromise->information.end << " for sample " << region->sampleId); } #endif @@ -1091,16 +1091,13 @@ void sfz::Voice::updateLoopInformation() noexcept if (!region->shouldLoop()) return; + const auto& info = currentPromise->information; + const auto rate = info.sampleRate; - const auto factor = currentPromise->oversamplingFactor; - const auto rate = currentPromise->sampleRate; - - loop.end = static_cast(region->loopEnd(factor)); - loop.start = static_cast(region->loopStart(factor)); + loop.end = static_cast(info.loopEnd); + loop.start = static_cast(info.loopBegin); loop.size = loop.end + 1 - loop.start; - loop.xfSize = static_cast( - lroundPositive(region->loopCrossfade * static_cast(factor) * rate) - ); + loop.xfSize = static_cast(lroundPositive(region->loopCrossfade * rate)); loop.xfOutStart = loop.end + 1 - loop.xfSize; loop.xfInStart = loop.start - loop.xfSize; } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index dbcae7d7..bc0b66ff 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -527,7 +527,7 @@ private: */ void updateLoopInformation() noexcept; - FilePromisePtr currentPromise { nullptr }; + FileDataHolder currentPromise; int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index c8cba91c..a5655ce6 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -525,10 +525,10 @@ bool WavetablePool::createFileWave(FilePool& filePool, const std::string& filena if (fileHandle->information.numChannels > 1) DBG("[sfizz] Only the first channel of " << filename << " will be used to create the wavetable"); - auto audioData = fileHandle->preloadedData->getConstSpan(0); + auto audioData = fileHandle->preloadedData.getConstSpan(0); // an even size is required for FFT - static_assert(absl::remove_reference_tpreloadedData)>::PaddingRight > 0, + static_assert(absl::remove_reference_tpreloadedData)>::PaddingRight > 0, "Right padding is required on the audio file buffer"); if (audioData.size() & 1) audioData = absl::MakeConstSpan(audioData.data(), audioData.size() + 1); From e693e82a396212ac1dd4812def6f6a5e25cb5750 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 19 Sep 2020 01:17:07 +0200 Subject: [PATCH 393/445] The dispatcher was not spinning --- src/sfizz/FilePool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index ec9dee51..05bd0770 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -374,7 +374,7 @@ void sfz::FilePool::loadingJob(QueuedFileData data) noexcept FileData::Status currentStatus = data.data->status.load(); unsigned spinCounter { 0 }; - if (currentStatus == FileData::Status::Invalid) { + while (currentStatus == FileData::Status::Invalid) { // Spin until the state changes if (spinCounter > 1024) { DBG("[sfizz] " << data.id << " is stuck on Invalid? Leaving the load"); From 9e0a1dff72c41fbe52b1c918aeeb3688fcb3e7cd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 19 Sep 2020 02:14:26 +0200 Subject: [PATCH 394/445] Add constructors to the QueuedFileData Honestly a stupid queue would be fine at this point --- src/sfizz/FilePool.h | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 53846ad1..84cf06ed 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -338,9 +338,17 @@ private: // Structures for the background loaders struct QueuedFileData { - FileId id; - FileData* data; - std::chrono::time_point queuedTime; + using TimePoint = std::chrono::time_point; + QueuedFileData() = default; + QueuedFileData(FileId id, FileData* data, TimePoint queuedTime) + : id(id), data(data), queuedTime(queuedTime) {} + QueuedFileData(const QueuedFileData&) = default; + QueuedFileData& operator=(const QueuedFileData&) = default; + QueuedFileData(QueuedFileData&&) = default; + QueuedFileData& operator=(QueuedFileData&&) = default; + FileId id {}; + FileData* data { nullptr }; + TimePoint queuedTime {}; }; atomic_queue::AtomicQueue2 filesToLoad; void dispatchingJob() noexcept; From a505b9f037a1827c11e71ae690815977c61c8a52 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 19 Sep 2020 02:46:42 +0200 Subject: [PATCH 395/445] gcc 4.9 idiosyncrasies --- src/sfizz/MathHelpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index d5b0cd3a..9fb035f1 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -674,7 +674,7 @@ public: } private: - std::array seeds_ {}; + std::array seeds_ {{}}; float mean_ { 0 }; float gain_ { 0 }; }; From d35ff4eb422ccc2c0fb2875e3b866df93cd4aac3 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 21 Sep 2020 09:21:06 +0200 Subject: [PATCH 396/445] Add a volatile on the stop flag in the thread pool --- src/external/threadpool/ThreadPool.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/external/threadpool/ThreadPool.h b/src/external/threadpool/ThreadPool.h index 36169849..2e030687 100644 --- a/src/external/threadpool/ThreadPool.h +++ b/src/external/threadpool/ThreadPool.h @@ -29,7 +29,7 @@ private: // synchronization std::mutex queue_mutex; std::condition_variable condition; - bool stop; + volatile bool stop; }; // the constructor just launches some amount of workers From 1e2ae6bce79ee99400928fb6407b979c1689c332 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 21 Sep 2020 09:37:19 +0200 Subject: [PATCH 397/445] Unused variable and a wrong name --- src/sfizz/FilePool.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 05bd0770..543c9bf6 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -99,7 +99,7 @@ sfz::FilePool::FilePool(sfz::Logger& logger) : logger(logger) { loadingJobs.reserve(config::maxVoices); - loadingJobs.reserve(config::maxVoices); + lastUsedFiles.reserve(config::maxVoices); garbageToCollect.reserve(config::maxVoices); } @@ -491,8 +491,6 @@ void sfz::FilePool::dispatchingJob() noexcept void sfz::FilePool::garbageJob() noexcept { - std::chrono::seconds counter { 0 }; // This avoids waiting to long on the thread - constexpr std::chrono::milliseconds timeAtom { 100 }; while (garbageFlag) { semGarbageBarrier.wait(); { From a0ad3b992123494e99a9d455a52b84fc0536a2a9 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 23 Sep 2020 12:00:11 +0200 Subject: [PATCH 398/445] Adapt the thread pool to the number of concurrent threads --- src/sfizz/FilePool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 543c9bf6..172b57c5 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -47,7 +47,7 @@ #endif #include "threadpool/ThreadPool.h" using namespace std::placeholders; -static ThreadPool threadPool { sfz::config::numBackgroundThreads }; +static ThreadPool threadPool { std::thread::hardware_concurrency() > 2 ? std::thread::hardware_concurrency() - 2 : 1 }; void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32_t numFrames) { From b78fa39d9e801a20607da8ee4ebdd82b5fdce92b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 7 Oct 2020 00:51:18 +0200 Subject: [PATCH 399/445] Remove unused variable --- src/sfizz/FilePool.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 172b57c5..6c3a08df 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -349,7 +349,6 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept // Update all the preloaded sizes for (auto& preloadedFile : preloadedFiles) { const auto maxOffset = preloadedFile.second.information.maxOffset; - const auto numFrames = preloadedFile.second.preloadedData.getNumFrames() / static_cast(oversamplingFactor); fs::path file { rootDirectory / preloadedFile.first.filename() }; AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, oversamplingFactor); From 81fe043bb3cb2bf97f7812228ab663a21252d903 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 7 Oct 2020 09:01:48 +0200 Subject: [PATCH 400/445] No need to ramp out killed voice data --- src/sfizz/Synth.cpp | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 2348dcb4..f4caa9a4 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -844,20 +844,20 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept ModMatrix& mm = resources.modMatrix; mm.beginCycle(numFrames); + { // Clear effect busses + ScopedTiming logger { callbackBreakdown.effects }; + for (auto& bus : effectBuses) { + if (bus) + bus->clearInputs(numFrames); + } + } + activeVoices = 0; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; tempMixSpan->fill(0.0f); resources.filePool.cleanupPromises(); - // Ramp out whatever is in the buffer at this point; should only be killed voice data - linearRamp(*rampSpan, 1.0f, -1.0f / static_cast(numFrames)); - for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { - if (auto& bus = effectBuses[i]) { - bus->applyGain(rampSpan->data(), numFrames); - } - } - for (auto& voice : voices) { if (voice->isFree()) continue; @@ -914,14 +914,6 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept // Reset the dispatch counter dispatchDuration = Duration(0); - { // Clear for the next run - ScopedTiming logger { callbackBreakdown.effects }; - for (auto& bus : effectBuses) { - if (bus) - bus->clearInputs(numFrames); - } - } - ASSERT(!hasNanInf(buffer.getConstSpan(0))); ASSERT(!hasNanInf(buffer.getConstSpan(1))); SFIZZ_CHECK(isReasonableAudio(buffer.getConstSpan(0))); From f9178d41aaf85a6b9757724632b41f7a1a88da91 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 7 Oct 2020 09:08:01 +0200 Subject: [PATCH 401/445] Remove the renderVoiceToOutput method and add a missing comment --- src/sfizz/Synth.cpp | 26 +++++++++++--------------- src/sfizz/Synth.h | 7 ++----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f4caa9a4..0b37df6d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -801,20 +801,6 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept } } -void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept -{ - const Region* region = voice.getRegion(); - ASSERT(region != nullptr); - - voice.renderBlock(tempSpan); - for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { - if (auto& bus = effectBuses[i]) { - float addGain = region->getGainToEffectBus(i); - bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames()); - } - } -} - void sfz::Synth::renderBlock(AudioSpan buffer) noexcept { ScopedFTZ ftz; @@ -865,7 +851,17 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept mm.beginVoice(voice->getId(), voice->getRegion()->getId(), voice->getTriggerEvent().value); activeVoices++; - renderVoiceToOutputs(*voice, *tempSpan); + + const Region* region = voice->getRegion(); + ASSERT(region != nullptr); + + voice->renderBlock(*tempSpan); + for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { + if (auto& bus = effectBuses[i]) { + float addGain = region->getGainToEffectBus(i); + bus->addToInputs(*tempSpan, addGain, numFrames); + } + } callbackBreakdown.data += voice->getLastDataDuration(); callbackBreakdown.amplitude += voice->getLastAmplitudeDuration(); callbackBreakdown.filters += voice->getLastFilterDuration(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index af3aa469..48aa69f7 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -742,13 +742,10 @@ private: void setupModMatrix(); /** - * @brief Render the voice to its designated outputs and effect busses. + * @brief Get the modification time of all included sfz files * - * @param voice - * @param tempSpan a temporary span used for rendering + * @return fs::file_time_type */ - void renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept; - fs::file_time_type checkModificationTime(); /** From 54d2b7342cf16770fdc2160f771c0ecb37e0505f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 7 Oct 2020 13:19:07 +0200 Subject: [PATCH 402/445] Clamp the cutoff values to avoid filters blowing up --- src/sfizz/FilterPool.cpp | 1 + src/sfizz/SIMDHelpers.h | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 2463e721..d8c16e40 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -75,6 +75,7 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned for (size_t i = 0; i < numFrames; ++i) (*cutoffSpan)[i] *= centsFactor(mod[i]); } + sfz::clampAll(*cutoffSpan, Default::filterCutoffRange); fill(*resonanceSpan, baseResonance); if (float* mod = mm.getModulation(resonanceTarget)) diff --git a/src/sfizz/SIMDHelpers.h b/src/sfizz/SIMDHelpers.h index 08371613..a7b9a08c 100644 --- a/src/sfizz/SIMDHelpers.h +++ b/src/sfizz/SIMDHelpers.h @@ -26,6 +26,7 @@ #pragma once #include "Config.h" #include "Debug.h" +#include "Range.h" #include "MathHelpers.h" #include "simd/HelpersScalar.h" #include @@ -720,6 +721,12 @@ void clampAll(absl::Span input, T low, T high) noexcept clampAll(input.data(), low, high, input.size()); } +template +void clampAll(absl::Span input, sfz::Range range) noexcept +{ + clampAll(input.data(), range.getStart(), range.getEnd(), input.size()); +} + /** * @brief Check that all values are within bounds (inclusive) * From 162673939a092f9d775e47df332792306f741655 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 7 Oct 2020 14:36:27 +0200 Subject: [PATCH 403/445] Map CC11 expression --- src/sfizz/Synth.cpp | 7 +++++++ tests/ModulationsT.cpp | 5 +++++ tests/TestHelpers.cpp | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 1a2f7183..ed183bfd 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -233,10 +233,12 @@ void sfz::Synth::clear() fill(absl::MakeSpan(ccInitialValues), 0.0f); initCc(7, 100); // volume initHdcc(10, 0.5f); // pan + initHdcc(11, 1.0f); // expression // set default controller labels insertPairUniquely(ccLabels, 7, "Volume"); insertPairUniquely(ccLabels, 10, "Pan"); + insertPairUniquely(ccLabels, 11, "Expression"); } void sfz::Synth::handleMasterOpcodes(const std::vector& members) @@ -676,6 +678,11 @@ void sfz::Synth::finalizeSfzLoad() ModKey::createCC(10, 1, defaultSmoothness, 0), ModKey::createNXYZ(ModId::Pan, region.id)).sourceDepth = 100.0f; } + if (!usedCCs.test(11)) { + region.getOrCreateConnection( + ModKey::createCC(11, 4, defaultSmoothness, 0), + ModKey::createNXYZ(ModId::Amplitude, region.id)).sourceDepth = 100.0f; + } } modificationTime = checkModificationTime(); diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index d5dc49af..0e1a863b 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -249,6 +249,7 @@ TEST_CASE("[Modulations] FlexEG Ampeg target") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createModulationDotGraph({ R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 11 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("EG 1 {0}" -> "MasterAmplitude {0}")", })); @@ -273,6 +274,7 @@ TEST_CASE("[Modulations] FlexEG Ampeg target with 2 FlexEGs") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createModulationDotGraph({ R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 11 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("EG 2 {0}" -> "MasterAmplitude {0}")", })); @@ -299,6 +301,7 @@ TEST_CASE("[Modulations] FlexEG Ampeg target with multiple EGs targeting ampeg") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createModulationDotGraph({ R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 11 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("EG 1 {0}" -> "MasterAmplitude {0}")", })); @@ -316,6 +319,7 @@ TEST_CASE("[Modulations] Override the default volume controller") REQUIRE(graph == createModulationDotGraph({ R"("AmplitudeEG {0}" -> "MasterAmplitude {0}")", R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {0}")", + R"("Controller 11 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("Controller 7 {curve=0, smooth=0, step=0}" -> "Pitch {0}")", })); } @@ -331,6 +335,7 @@ TEST_CASE("[Modulations] Override the default pan controller") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createModulationDotGraph({ R"("AmplitudeEG {0}" -> "MasterAmplitude {0}")", + R"("Controller 11 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", })); } diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index c5b3691d..0f3f76f6 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -97,6 +97,11 @@ std::string createDefaultGraph(std::vector lines, int numRegions) regionIdx, R"(}")" )); + lines.push_back(absl::StrCat( + R"("Controller 11 {curve=4, smooth=10, step=0}" -> "Amplitude {)", + regionIdx, + R"(}")" + )); } return createModulationDotGraph(lines); From 3aeb96d3b2ec81de37c4f6117d2201633e63ec05 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 7 Oct 2020 14:41:16 +0200 Subject: [PATCH 404/445] Fix a compilation warning, about FileData not copyable --- src/sfizz/FilePool.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 84cf06ed..109394b4 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -80,8 +80,8 @@ struct FileData return AudioSpan(preloadedData); } - FileData(const FileData& other) = default; - FileData& operator=(const FileData& other) = default; + FileData(const FileData& other) = delete; + FileData& operator=(const FileData& other) = delete; FileData(FileData&& other) { ASSERT(other.readerCount == 0); // Probably should not be moving this... From b76a1c50a39f73710e48155213f90439ae27ce55 Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Thu, 8 Oct 2020 02:10:00 +0900 Subject: [PATCH 405/445] accept samplesPerBlock up to 8192 (inclusive) `ASSERT(samplesPerBlock < config::maxBlockSize)` (where `maxBlockSize` is 8192) means we cannot set 8192. But we don't want 8191 as the actual maximum size. --- src/sfizz/Synth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 1c1c4278..3a872727 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -776,7 +776,7 @@ void sfz::Synth::garbageCollect() noexcept void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept { - ASSERT(samplesPerBlock < config::maxBlockSize); + ASSERT(samplesPerBlock <= config::maxBlockSize); const std::lock_guard disableCallback { callbackGuard }; From a16a835850bba11730d9fcdaa815b625c6fd7037 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 7 Oct 2020 19:47:29 +0200 Subject: [PATCH 406/445] Add @atsushieno to the list of contributors --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index f2b6beb1..4a97b0ed 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -9,3 +9,4 @@ Contributors to `sfizz`, in chronologic order: - Jean-Pierre Cimalando (2020) - Tobiasz "unfa" KaroÅ„ (2020) - Kinwie (2020) +- Atsushi Eno (2020) From a3238f27c1b709b3c9c2e36ad8a902c9a4eea0e5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 7 Oct 2020 20:16:32 +0200 Subject: [PATCH 407/445] Removal of redundant information [ci skip] --- LICENSE.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 25c59de3..d3f769ae 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,12 +3,7 @@ BSD 2-Clause License The code is copyrighted by their respective authors, as indicated by the source control mechanism. -Contributors include: -- Paul Ferrand (2019-) paul at ferrand dot cc -- Andrea Zanellato (2019-) -- Jean-Pierre Cimalando (2020-) -- Michael Willis (2020-) -- Alexander Mitchell (2020-) +Please refer to AUTHORS.md for the list of contributors. All rights reserved. From aea430cbfd9cff193a6ae27362dcfb2912d43f13 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 9 Oct 2020 13:55:53 +0200 Subject: [PATCH 408/445] Do not force a vcpkg update --- appveyor.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 3bb7f7f6..ea480c91 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,10 +12,10 @@ install: - cmd: set PATH=C:\Program Files (x86)\Inno Setup 6;%PATH% - cmd: if %platform%==Win32 set VCPKG_TRIPLET=x86-windows-static - cmd: if %platform%==x64 set VCPKG_TRIPLET=x64-windows-static -- cmd: cd c:\tools\vcpkg\ -- cmd: git pull -- cmd: .\bootstrap-vcpkg.bat -- cmd: cd %APPVEYOR_BUILD_FOLDER% +# - cmd: cd c:\tools\vcpkg\ +# - cmd: git pull +# - cmd: .\bootstrap-vcpkg.bat +# - cmd: cd %APPVEYOR_BUILD_FOLDER% - cmd: vcpkg install libsndfile:%VCPKG_TRIPLET% before_build: From b27a509aa973ebe51adce2a84997c11b18755bba Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 9 Oct 2020 14:00:13 +0200 Subject: [PATCH 409/445] Define the build options before including SfizzConfig --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0158e93a..19aa16d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,6 @@ set (PROJECT_DESCRIPTION "A library to load SFZ description files and use them t # External configuration CMake scripts set (CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_SOURCE_DIR}/cmake") include (BuildType) -include (SfizzConfig) # Build Options set (BUILD_TESTING OFF CACHE BOOL "Disable Abseil's tests [default: OFF]") @@ -34,6 +33,8 @@ option (SFIZZ_USE_VCPKG "Assume that sfizz is build using vcpkg [default option (SFIZZ_STATIC_DEPENDENCIES "Link dependencies statically [default: OFF]" OFF) option (SFIZZ_RELEASE_ASSERTS "Forced assertions in release builds [default: OFF]" OFF) +include (SfizzConfig) + # Don't use IPO in non Release builds include (CheckIPO) From f333013dff2fd729ef5d6de5c7d92658e7efac13 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 02:43:27 +0200 Subject: [PATCH 410/445] Eliminate a few warnings --- benchmarks/BM_filterStereoMono.cpp | 2 +- benchmarks/BM_flacfile.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/BM_filterStereoMono.cpp b/benchmarks/BM_filterStereoMono.cpp index 16185812..43030a24 100644 --- a/benchmarks/BM_filterStereoMono.cpp +++ b/benchmarks/BM_filterStereoMono.cpp @@ -20,7 +20,7 @@ constexpr float sampleRate { 48000.0f }; class FilterFixture : public benchmark::Fixture { public: - void SetUp(const ::benchmark::State& state) { + void SetUp(const ::benchmark::State& /* state */) { inputLeft = std::vector(blockSize); inputRight = std::vector(blockSize); outputLeft = std::vector(blockSize); diff --git a/benchmarks/BM_flacfile.cpp b/benchmarks/BM_flacfile.cpp index 9d17de3a..4fedae8c 100644 --- a/benchmarks/BM_flacfile.cpp +++ b/benchmarks/BM_flacfile.cpp @@ -19,7 +19,7 @@ class FileFixture : public benchmark::Fixture { public: - void SetUp(const ::benchmark::State& state) { + void SetUp(const ::benchmark::State& /* state */) { filePath1 = getPath() / "sample1.flac"; filePath2 = getPath() / "sample2.flac"; filePath3 = getPath() / "sample3.flac"; From 80ee358523929fa2dc3a517d8ed9f6f1858dba68 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 03:07:07 +0200 Subject: [PATCH 411/445] Fix the class-memaccess warning --- src/sfizz/modulations/ModKey.cpp | 14 ++++++++--- src/sfizz/modulations/ModKey.h | 41 ++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 9750070c..051a4f8e 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -15,18 +15,26 @@ ModKey::Parameters::Parameters() noexcept // zero-fill the structure // 1. this ensures that non-used values will be always 0 // 2. this makes the object memcmp-comparable - std::memset(this, 0, sizeof(*this)); + std::memset( + static_cast(this), + 0, sizeof(RawParameters)); } ModKey::Parameters::Parameters(const Parameters& other) noexcept { - std::memcpy(this, &other, sizeof(*this)); + std::memcpy( + static_cast(this), + static_cast(&other), + sizeof(RawParameters)); } ModKey::Parameters& ModKey::Parameters::operator=(const Parameters& other) noexcept { if (this != &other) - std::memcpy(this, &other, sizeof(*this)); + std::memcpy( + static_cast(this), + static_cast(&other), + sizeof(RawParameters)); return *this; } diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index e3f484c4..ce16c7a2 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -42,24 +42,7 @@ public: bool isTarget() const noexcept; std::string toString() const; - struct Parameters { - Parameters() noexcept; - Parameters(const Parameters& other) noexcept; - Parameters& operator=(const Parameters& other) noexcept; - - Parameters(Parameters&&) = delete; - Parameters &operator=(Parameters&&) = delete; - - bool operator==(const Parameters& other) const noexcept - { - return std::memcmp(this, &other, sizeof(*this)) == 0; - } - - bool operator!=(const Parameters& other) const noexcept - { - return std::memcmp(this, &other, sizeof(*this)) != 0; - } - + struct RawParameters { union { //! Parameters if this key identifies a CC source struct { uint16_t cc; uint8_t curve, smooth; float step; }; @@ -71,6 +54,28 @@ public: }; }; + struct Parameters : RawParameters { + Parameters() noexcept; + Parameters(const Parameters& other) noexcept; + Parameters& operator=(const Parameters& other) noexcept; + + Parameters(Parameters&&) = delete; + Parameters &operator=(Parameters&&) = delete; + + bool operator==(const Parameters& other) const noexcept + { + return std::memcmp( + static_cast(this), + static_cast(&other), + sizeof(RawParameters)) == 0; + } + + bool operator!=(const Parameters& other) const noexcept + { + return !operator==(other); + } + }; + public: bool operator==(const ModKey &other) const noexcept { From 68d4e65ea53eb2b92d936c4e5da70b0d79eca5d5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 16:15:41 +0200 Subject: [PATCH 412/445] Locally ignored "-Wmultichar" not working on gcc, make global --- cmake/SfizzConfig.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 95b0adaa..2b2cdd4a 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -57,6 +57,7 @@ endif() if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Wall) add_compile_options(-Wextra) + add_compile_options(-Wno-multichar) add_compile_options(-Werror=return-type) if (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(i.86|x86_64)$") add_compile_options(-msse2) From 6af2bc5bc09c065efeab91524a6f14d89f26b310 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 16:17:01 +0200 Subject: [PATCH 413/445] Remove some functions no longer used --- editor/tools/layout-maker/sources/reader.cpp | 61 -------------------- 1 file changed, 61 deletions(-) diff --git a/editor/tools/layout-maker/sources/reader.cpp b/editor/tools/layout-maker/sources/reader.cpp index 788f93d7..5890951b 100644 --- a/editor/tools/layout-maker/sources/reader.cpp +++ b/editor/tools/layout-maker/sources/reader.cpp @@ -86,67 +86,6 @@ static int consume_real_token(TokenList::iterator &tok_it, TokenList::iterator t return std::stod(text); } -static void consume_image_properties(LayoutImage &image, TokenList::iterator &tok_it, TokenList::iterator tok_end) -{ - for (bool have = true; have;) { - if (try_consume_next_token("xywh", tok_it, tok_end)) { - ensure_next_token("{", tok_it, tok_end); - image.x = consume_int_token(tok_it, tok_end); - image.y = consume_int_token(tok_it, tok_end); - image.w = consume_int_token(tok_it, tok_end); - image.h = consume_int_token(tok_it, tok_end); - ensure_next_token("}", tok_it, tok_end); - } - else - have = false; - } -} - -// static void consume_layout_item_properties(LayoutItem &item, TokenList::iterator &tok_it, TokenList::iterator tok_end) -// { -// ensure_next_token("{", tok_it, tok_end); -// for (std::string text; (text = consume_next_token(tok_it, tok_end)) != "}";) { -// if (text == "open" || text == "selected") -// ; // skip -// else if (text == "label") -// item.label = consume_any_string(tok_it, tok_end); -// else if (text == "xywh") { -// ensure_next_token("{", tok_it, tok_end); -// item.x = consume_int_token(tok_it, tok_end); -// item.y = consume_int_token(tok_it, tok_end); -// item.w = consume_int_token(tok_it, tok_end); -// item.h = consume_int_token(tok_it, tok_end); -// ensure_next_token("}", tok_it, tok_end); -// } -// else if (text == "box") -// item.box = consume_next_token(tok_it, tok_end); -// else if (text == "labelfont") -// item.labelfont = consume_int_token(tok_it, tok_end); -// else if (text == "labelsize") -// item.labelsize = consume_int_token(tok_it, tok_end); -// else if (text == "labeltype") -// item.labeltype = consume_any_string(tok_it, tok_end); -// else if (text == "align") -// item.align = consume_int_token(tok_it, tok_end); -// else if (text == "type") -// item.type = consume_any_string(tok_it, tok_end); -// else if (text == "callback") -// item.callback = consume_any_string(tok_it, tok_end); -// else if (text == "class") -// item.classname = consume_any_string(tok_it, tok_end); -// else if (text == "minimum") -// item.minimum = consume_real_token(tok_it, tok_end); -// else if (text == "maximum") -// item.maximum = consume_real_token(tok_it, tok_end); -// else if (text == "step") -// item.step = consume_real_token(tok_it, tok_end); -// else if (text == "image") { -// item.image.filepath = consume_any_string(tok_it, tok_end); -// consume_image_properties(item.image, tok_it, tok_end); -// } -// } -// } - static void consume_layout_item_properties(LayoutItem &item, TokenList::iterator &tok_it, TokenList::iterator tok_end) { ensure_next_token("{", tok_it, tok_end); From 0f426786d23f433dacbdc027eaaffb283e9e57e9 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 16:41:50 +0200 Subject: [PATCH 414/445] Update vstgui to eliminate the CRect warning --- editor/external/vstgui4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index a8a546b8..dbb9a427 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit a8a546b89ebef7e263125b2f5859a3a365884cc6 +Subproject commit dbb9a42742eb42826f2fe15bd047bae0adb13b58 From c17a3f855441dadc262decf89234b227ea42b7a0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 16:44:11 +0200 Subject: [PATCH 415/445] Eliminate -Wparentheses in LV2 --- lv2/lv2/atom/util.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lv2/lv2/atom/util.h b/lv2/lv2/atom/util.h index 051a3cb2..9c372aab 100644 --- a/lv2/lv2/atom/util.h +++ b/lv2/lv2/atom/util.h @@ -122,13 +122,13 @@ lv2_atom_sequence_next(const LV2_Atom_Event* i) @endcode */ #define LV2_ATOM_SEQUENCE_FOREACH(seq, iter) \ - for (LV2_Atom_Event* (iter) = lv2_atom_sequence_begin(&(seq)->body); \ + for (LV2_Atom_Event* iter = lv2_atom_sequence_begin(&(seq)->body); \ !lv2_atom_sequence_is_end(&(seq)->body, (seq)->atom.size, (iter)); \ (iter) = lv2_atom_sequence_next(iter)) /** Like LV2_ATOM_SEQUENCE_FOREACH but for a headerless sequence body. */ #define LV2_ATOM_SEQUENCE_BODY_FOREACH(body, size, iter) \ - for (LV2_Atom_Event* (iter) = lv2_atom_sequence_begin(body); \ + for (LV2_Atom_Event* iter = lv2_atom_sequence_begin(body); \ !lv2_atom_sequence_is_end(body, size, (iter)); \ (iter) = lv2_atom_sequence_next(iter)) @@ -219,13 +219,13 @@ lv2_atom_tuple_next(const LV2_Atom* i) @endcode */ #define LV2_ATOM_TUPLE_FOREACH(tuple, iter) \ - for (LV2_Atom* (iter) = lv2_atom_tuple_begin(tuple); \ + for (LV2_Atom* iter = lv2_atom_tuple_begin(tuple); \ !lv2_atom_tuple_is_end(LV2_ATOM_BODY(tuple), (tuple)->atom.size, (iter)); \ (iter) = lv2_atom_tuple_next(iter)) /** Like LV2_ATOM_TUPLE_FOREACH but for a headerless tuple body. */ #define LV2_ATOM_TUPLE_BODY_FOREACH(body, size, iter) \ - for (LV2_Atom* (iter) = (LV2_Atom*)(body); \ + for (LV2_Atom* iter = (LV2_Atom*)(body); \ !lv2_atom_tuple_is_end(body, size, (iter)); \ (iter) = lv2_atom_tuple_next(iter)) @@ -275,13 +275,13 @@ lv2_atom_object_next(const LV2_Atom_Property_Body* i) @endcode */ #define LV2_ATOM_OBJECT_FOREACH(obj, iter) \ - for (LV2_Atom_Property_Body* (iter) = lv2_atom_object_begin(&(obj)->body); \ + for (LV2_Atom_Property_Body* iter = lv2_atom_object_begin(&(obj)->body); \ !lv2_atom_object_is_end(&(obj)->body, (obj)->atom.size, (iter)); \ (iter) = lv2_atom_object_next(iter)) /** Like LV2_ATOM_OBJECT_FOREACH but for a headerless object body. */ #define LV2_ATOM_OBJECT_BODY_FOREACH(body, size, iter) \ - for (LV2_Atom_Property_Body* (iter) = lv2_atom_object_begin(body); \ + for (LV2_Atom_Property_Body* iter = lv2_atom_object_begin(body); \ !lv2_atom_object_is_end(body, size, (iter)); \ (iter) = lv2_atom_object_next(iter)) From 191a23232777f899f7424168f41a9b335bdebb13 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 17:13:04 +0200 Subject: [PATCH 416/445] Eliminate a warning with win32 format --- editor/src/editor/Editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index b936065f..ed235ecc 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -590,7 +590,7 @@ void Editor::Impl::createFrameContents() for (int log2value = 10; log2value <= 16; ++log2value) { int value = 1 << log2value; char text[256]; - sprintf(text, "%lu kB", value / 1024 * sizeof(float)); + sprintf(text, "%lu kB", static_cast(value / 1024 * sizeof(float))); text[sizeof(text) - 1] = '\0'; preloadSizeSlider_->addEntry(text, value); } From 7a39d6c7cf2e5ee5ffd22d4e3edd267c3dd56ea1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 10 Oct 2020 17:15:45 +0200 Subject: [PATCH 417/445] Eliminate more warnings from Steinberg VST --- vst/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index cddf56b2..ecbbab4a 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -108,7 +108,8 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") "-Wno-unknown-pragmas" "-Wno-unused-function" "-Wno-unused-parameter" - "-Wno-unused-variable") + "-Wno-unused-variable" + "-Wno-format") endif() # To help debugging the link only @@ -292,7 +293,8 @@ elseif(SFIZZ_AU) "-Wno-unknown-pragmas" "-Wno-unused-function" "-Wno-unused-parameter" - "-Wno-unused-variable") + "-Wno-unused-variable" + "-Wno-format") endif() # Installation From 9386cd3336841782aedf12a5f2da7d0606778c05 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 11 Oct 2020 21:12:59 +0200 Subject: [PATCH 418/445] Implement the file drag and drop for X11 --- editor/cmake/Vstgui.cmake | 5 +++++ editor/external/vstgui4 | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 06031d80..b8dcdf08 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -55,6 +55,7 @@ add_library(sfizz-vstgui STATIC EXCLUDE_FROM_ALL "${VSTGUI_BASEDIR}/vstgui/lib/cvstguitimer.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/genericstringlistdatabrowsersource.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/common/genericoptionmenu.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/platformfactory.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/vstguidebug.cpp") if(WIN32) @@ -71,6 +72,7 @@ if(WIN32) "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32optionmenu.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32support.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32textedit.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32factory.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/winfileselector.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/winstring.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/wintimer.cpp") @@ -94,6 +96,7 @@ elseif(APPLE) "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/nsviewframe.mm" "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/cocoa/nsviewoptionmenu.mm" "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macclipboard.mm" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macfactory.mm" "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macfileselector.mm" "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macglobals.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/mac/macstring.mm" @@ -108,7 +111,9 @@ else() "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairofont.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairogradient.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/cairopath.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/linuxfactory.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/linuxstring.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11dragging.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11fileselector.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11frame.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/linux/x11platform.cpp" diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index dbb9a427..055cbcc9 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit dbb9a42742eb42826f2fe15bd047bae0adb13b58 +Subproject commit 055cbcc9ae858f0b07d5d86c205a1111e2fba7a4 From ecc223947c8bbb485c755c7e2dae3c0abb97fb9a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 11 Oct 2020 21:33:26 +0200 Subject: [PATCH 419/445] Link Vstgui to Glib on X11 --- editor/cmake/Vstgui.cmake | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index b8dcdf08..022e4da5 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -165,6 +165,7 @@ else() pkg_check_modules(LIBXKB_COMMON_X11 REQUIRED xkbcommon-x11) pkg_check_modules(CAIRO REQUIRED cairo) pkg_check_modules(FONTCONFIG REQUIRED fontconfig) + pkg_check_modules(GLIB REQUIRED glib-2.0) target_include_directories(sfizz-vstgui PRIVATE ${X11_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} @@ -176,7 +177,8 @@ else() ${LIBXKB_COMMON_INCLUDE_DIRS} ${LIBXKB_COMMON_X11_INCLUDE_DIRS} ${CAIRO_INCLUDE_DIRS} - ${FONTCONFIG_INCLUDE_DIRS}) + ${FONTCONFIG_INCLUDE_DIRS} + ${GLIB_INCLUDE_DIRS}) target_link_libraries(sfizz-vstgui PRIVATE ${X11_LIBRARIES} ${FREETYPE_LIBRARIES} @@ -188,7 +190,8 @@ else() ${LIBXKB_COMMON_LIBRARIES} ${LIBXKB_COMMON_X11_LIBRARIES} ${CAIRO_LIBRARIES} - ${FONTCONFIG_LIBRARIES}) + ${FONTCONFIG_LIBRARIES} + ${GLIB_LIBRARIES}) find_library(DL_LIBRARY "dl") if(DL_LIBRARY) target_link_libraries(sfizz-vstgui PRIVATE "${DL_LIBRARY}") From 54b24f7cd1804ce3a4c1eec4ad5b48620e7b7f12 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 11 Oct 2020 21:38:31 +0200 Subject: [PATCH 420/445] Update the Vstgui source list (win32) --- editor/cmake/Vstgui.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 022e4da5..4e37f19c 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -71,6 +71,7 @@ if(WIN32) "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32openglview.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32optionmenu.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32support.cpp" + "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32resourcestream.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32textedit.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/win32factory.cpp" "${VSTGUI_BASEDIR}/vstgui/lib/platform/win32/winfileselector.cpp" From 777bcf7b17cd224e4a11a9a5e0af838eb0155991 Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Mon, 12 Oct 2020 21:47:48 +0900 Subject: [PATCH 421/445] Fix build on Android 32-bit arm: hardfp is not available context: https://github.com/sfztools/sfizz/commit/d783ba6c8715ebb19124d713bf3223c9f4946691#r43170024 This fixes build errors like this on Android 32-bit arm architecture: `error: lv2/CMakeFiles/sfizz_lv2.dir/sfizz.c.o uses VFP register arguments, output does not` --- cmake/SfizzConfig.cmake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 2b2cdd4a..b488b4db 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -63,7 +63,9 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-msse2) elseif(SFIZZ_SYSTEM_PROCESSOR MATCHES "^(arm.*)$") add_compile_options(-mfpu=neon) - add_compile_options(-mfloat-abi=hard) + if (NOT ANDROID) + add_compile_options(-mfloat-abi=hard) + endif() endif() elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") set(CMAKE_CXX_STANDARD 17) From ba2f8ab97fd15e5b8da85b62e8667ffb3c511189 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 12 Oct 2020 15:17:17 +0200 Subject: [PATCH 422/445] Allow Vstgui to link on MSYS MinGW where find_library fails --- editor/cmake/Vstgui.cmake | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 4e37f19c..0ad30b36 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -127,19 +127,13 @@ target_include_directories(sfizz-vstgui PUBLIC "${VSTGUI_BASEDIR}") if(WIN32) if (NOT MSVC) # autolinked on MSVC with pragmas - find_library(OPENGL32_LIBRARY "opengl32") - find_library(D2D1_LIBRARY "d2d1") - find_library(DWRITE_LIBRARY "dwrite") - find_library(DWMAPI_LIBRARY "dwmapi") - find_library(WINDOWSCODECS_LIBRARY "windowscodecs") - find_library(SHLWAPI_LIBRARY "shlwapi") target_link_libraries(sfizz-vstgui PRIVATE - "${OPENGL32_LIBRARY}" - "${D2D1_LIBRARY}" - "${DWRITE_LIBRARY}" - "${DWMAPI_LIBRARY}" - "${WINDOWSCODECS_LIBRARY}" - "${SHLWAPI_LIBRARY}") + "opengl32" + "d2d1" + "dwrite" + "dwmapi" + "windowscodecs" + "shlwapi") endif() elseif(APPLE) target_link_libraries(sfizz-vstgui PRIVATE From e2dc18deb64c7066a68a0a7e53f005b1daf44e94 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 12 Oct 2020 16:01:50 +0200 Subject: [PATCH 423/445] Prevent the editor from refreshing constantly --- editor/src/editor/Editor.cpp | 41 ++++++----------------------- editor/src/editor/GUIComponents.cpp | 22 ++++++++-------- 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index ed235ecc..4c16773d 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -183,20 +183,16 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) case EditId::Volume: { const float value = v.to_float(); - if (volumeSlider_) { + if (volumeSlider_) volumeSlider_->setValue(value); - volumeSlider_->setDirty(); - } updateVolumeLabel(value); } break; case EditId::Polyphony: { const int value = static_cast(v.to_float()); - if (numVoicesSlider_) { + if (numVoicesSlider_) numVoicesSlider_->setValue(value); - numVoicesSlider_->setDirty(); - } updateNumVoicesLabel(value); } break; @@ -208,20 +204,16 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) for (int f = value; f > 1; f /= 2) ++log2Value; - if (oversamplingSlider_) { + if (oversamplingSlider_) oversamplingSlider_->setValue(log2Value); - oversamplingSlider_->setDirty(); - } updateOversamplingLabel(log2Value); } break; case EditId::PreloadSize: { const int value = static_cast(v.to_float()); - if (preloadSizeSlider_) { + if (preloadSizeSlider_) preloadSizeSlider_->setValue(value); - preloadSizeSlider_->setDirty(); - } updatePreloadSizeLabel(value); } break; @@ -235,34 +227,26 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) case EditId::ScalaRootKey: { const int value = std::max(0, static_cast(v.to_float())); - if (scalaRootKeySlider_) { + if (scalaRootKeySlider_) scalaRootKeySlider_->setValue(value % 12); - scalaRootKeySlider_->setDirty(); - } - if (scalaRootOctaveSlider_) { + if (scalaRootOctaveSlider_) scalaRootOctaveSlider_->setValue(value / 12); - scalaRootOctaveSlider_->setDirty(); - } updateScalaRootKeyLabel(value); } break; case EditId::TuningFrequency: { const float value = v.to_float(); - if (tuningFrequencySlider_) { + if (tuningFrequencySlider_) tuningFrequencySlider_->setValue(value); - tuningFrequencySlider_->setDirty(); - } updateTuningFrequencyLabel(value); } break; case EditId::StretchTuning: { const float value = v.to_float(); - if (stretchedTuningSlider_) { + if (stretchedTuningSlider_) stretchedTuningSlider_->setValue(value); - stretchedTuningSlider_->setDirty(); - } updateStretchedTuningLabel(value); } break; @@ -746,7 +730,6 @@ void Editor::Impl::updateLabelWithFileName(CTextLabel* label, const std::string& std::string fileName = std::string(simplifiedFileName(filePath, removedSuffix, "")); label->setText(fileName.c_str()); - label->setDirty(); } void Editor::Impl::updateButtonWithFileName(CTextButton* button, const std::string& filePath, absl::string_view removedSuffix) @@ -756,7 +739,6 @@ void Editor::Impl::updateButtonWithFileName(CTextButton* button, const std::stri std::string fileName = std::string(simplifiedFileName(filePath, removedSuffix, "")); button->setTitle(fileName.c_str()); - button->setDirty(); } void Editor::Impl::updateVolumeLabel(float volume) @@ -769,7 +751,6 @@ void Editor::Impl::updateVolumeLabel(float volume) sprintf(text, "%.1f dB", volume); text[sizeof(text) - 1] = '\0'; label->setText(text); - label->setDirty(); } void Editor::Impl::updateNumVoicesLabel(int numVoices) @@ -782,7 +763,6 @@ void Editor::Impl::updateNumVoicesLabel(int numVoices) sprintf(text, "%d", numVoices); text[sizeof(text) - 1] = '\0'; label->setText(text); - label->setDirty(); } void Editor::Impl::updateOversamplingLabel(int oversamplingLog2) @@ -795,7 +775,6 @@ void Editor::Impl::updateOversamplingLabel(int oversamplingLog2) sprintf(text, "%dx", 1 << oversamplingLog2); text[sizeof(text) - 1] = '\0'; label->setText(text); - label->setDirty(); } void Editor::Impl::updatePreloadSizeLabel(int preloadSize) @@ -808,7 +787,6 @@ void Editor::Impl::updatePreloadSizeLabel(int preloadSize) sprintf(text, "%d kB", static_cast(std::round(preloadSize * (1.0 / 1024)))); text[sizeof(text) - 1] = '\0'; label->setText(text); - label->setDirty(); } void Editor::Impl::updateScalaRootKeyLabel(int rootKey) @@ -837,7 +815,6 @@ void Editor::Impl::updateScalaRootKeyLabel(int rootKey) }; label->setText(noteName(rootKey)); - label->setDirty(); } void Editor::Impl::updateTuningFrequencyLabel(float tuningFrequency) @@ -850,7 +827,6 @@ void Editor::Impl::updateTuningFrequencyLabel(float tuningFrequency) sprintf(text, "%.1f", tuningFrequency); text[sizeof(text) - 1] = '\0'; label->setText(text); - label->setDirty(); } void Editor::Impl::updateStretchedTuningLabel(float stretchedTuning) @@ -863,7 +839,6 @@ void Editor::Impl::updateStretchedTuningLabel(float stretchedTuning) sprintf(text, "%.3f", stretchedTuning); text[sizeof(text) - 1] = '\0'; label->setText(text); - label->setDirty(); } void Editor::Impl::setActivePanel(unsigned panelId) diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index d3e11b02..abf52702 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -24,13 +24,13 @@ SBoxContainer::SBoxContainer(const CRect& size) void SBoxContainer::setCornerRadius(CCoord radius) { cornerRadius_ = radius; - setDirty(); + invalid(); } void SBoxContainer::setBackgroundColor(const CColor& color) { backgroundColor_ = color; - setDirty(); + invalid(); } CColor SBoxContainer::getBackgroundColor() const @@ -62,19 +62,19 @@ STitleContainer::STitleContainer(const CRect& size, UTF8StringPtr text) void STitleContainer::setTitleFont(CFontRef font) { titleFont_ = font; - setDirty(); + invalid(); } void STitleContainer::setTitleFontColor(CColor color) { titleFontColor_ = color; - setDirty(); + invalid(); } void STitleContainer::setTitleBackgroundColor(CColor color) { titleBackgroundColor_ = color; - setDirty(); + invalid(); } void STitleContainer::drawRect(CDrawContext* dc, const CRect& updateRect) @@ -163,7 +163,7 @@ SPiano::SPiano(const CRect& bounds) void SPiano::setFont(CFontRef font) { font_ = font; - setDirty(); + invalid(); } void SPiano::clearKeyRanges() @@ -450,14 +450,14 @@ void STextButton::draw(CDrawContext* context) CMouseEventResult STextButton::onMouseEntered (CPoint& where, const CButtonState& buttons) { hovered = true; - setDirty(); + invalid(); return CTextButton::onMouseEntered(where, buttons); } CMouseEventResult STextButton::onMouseExited (CPoint& where, const CButtonState& buttons) { hovered = false; - setDirty(); + invalid(); return CTextButton::onMouseExited(where, buttons); } @@ -472,7 +472,7 @@ void SStyledKnob::setActiveTrackColor(const CColor& color) if (activeTrackColor_ == color) return; activeTrackColor_ = color; - setDirty(); + invalid(); } void SStyledKnob::setInactiveTrackColor(const CColor& color) @@ -480,7 +480,7 @@ void SStyledKnob::setInactiveTrackColor(const CColor& color) if (inactiveTrackColor_ == color) return; inactiveTrackColor_ = color; - setDirty(); + invalid(); } void SStyledKnob::setLineIndicatorColor(const CColor& color) @@ -488,7 +488,7 @@ void SStyledKnob::setLineIndicatorColor(const CColor& color) if (lineIndicatorColor_ == color) return; lineIndicatorColor_ = color; - setDirty(); + invalid(); } void SStyledKnob::draw(CDrawContext* dc) From 1e8b36c14d59db5d49cecd114d696abd4927dcb0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 12 Oct 2020 15:26:15 +0200 Subject: [PATCH 424/445] Do not install the static library, only shared with pkgconfig --- src/CMakeLists.txt | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7ffd3c1f..b4a55653 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -228,18 +228,6 @@ if (SFIZZ_RELEASE_ASSERTS) endif() sfizz_enable_fast_math(sfizz_static) -if (NOT MSVC) - install (TARGETS sfizz_static - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - COMPONENT "development") - - configure_file (${PROJECT_SOURCE_DIR}/scripts/sfizz.pc.in sfizz.pc @ONLY) - install (FILES ${CMAKE_BINARY_DIR}/src/sfizz.pc - DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig - COMPONENT "development") -endif() if(WIN32) include(VSTConfig) configure_file (${PROJECT_SOURCE_DIR}/scripts/innosetup.iss.in ${PROJECT_BINARY_DIR}/innosetup.iss @ONLY) @@ -265,15 +253,19 @@ if (SFIZZ_SHARED) target_compile_definitions (sfizz_shared PRIVATE "SFIZZ_ENABLE_RELEASE_ASSERT=1") endif() target_compile_definitions(sfizz_shared PRIVATE SFIZZ_EXPORT_SYMBOLS) - set_target_properties (sfizz_shared PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME sfizz) + set_target_properties (sfizz_shared PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp") sfizz_enable_lto_if_needed(sfizz_shared) sfizz_enable_fast_math(sfizz_shared) if (NOT MSVC) install (TARGETS sfizz_shared - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - COMPONENT "runtime") + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT "runtime" + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT "runtime" + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT "development" + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} COMPONENT "development") + configure_file (${PROJECT_SOURCE_DIR}/scripts/sfizz.pc.in sfizz.pc @ONLY) + install (FILES ${CMAKE_BINARY_DIR}/src/sfizz.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + COMPONENT "development") endif() endif() From 5e84f29e59c3ae924738a7f4b7f569b5c7b41fcf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 12 Oct 2020 20:22:48 +0200 Subject: [PATCH 425/445] Set a fixed number of parallel tasks --- .travis/script_library.sh | 2 +- .travis/script_mingw.sh | 4 ++-- .travis/script_moddevices.sh | 2 +- .travis/script_plugins.sh | 2 +- .travis/script_test.sh | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis/script_library.sh b/.travis/script_library.sh index 0d014f4e..20449edc 100755 --- a/.travis/script_library.sh +++ b/.travis/script_library.sh @@ -8,4 +8,4 @@ cmake -DCMAKE_BUILD_TYPE=Release \ -DSFIZZ_TESTS=OFF \ -DCMAKE_CXX_STANDARD=17 \ .. -make -j$(nproc) +make -j2 diff --git a/.travis/script_mingw.sh b/.travis/script_mingw.sh index e09db92a..85aac04e 100755 --- a/.travis/script_mingw.sh +++ b/.travis/script_mingw.sh @@ -12,7 +12,7 @@ if [[ ${CROSS_COMPILE} == "mingw32" ]]; then -DSFIZZ_STATIC_DEPENDENCIES=ON \ -DCMAKE_CXX_STANDARD=17 \ .. - buildenv make -j$(nproc) + buildenv make -j2 elif [[ ${CROSS_COMPILE} == "mingw64" ]]; then buildenv x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release \ -DENABLE_LTO=OFF \ @@ -21,5 +21,5 @@ elif [[ ${CROSS_COMPILE} == "mingw64" ]]; then -DSFIZZ_STATIC_DEPENDENCIES=ON \ -DCMAKE_CXX_STANDARD=17 \ .. - buildenv make -j$(nproc) + buildenv make -j2 fi diff --git a/.travis/script_moddevices.sh b/.travis/script_moddevices.sh index 278b7045..3654590d 100755 --- a/.travis/script_moddevices.sh +++ b/.travis/script_moddevices.sh @@ -8,4 +8,4 @@ mkdir -p build/${INSTALL_DIR} && cd build buildenv mod-plugin-builder /usr/local/bin/cmake \ -DSFIZZ_SYSTEM_PROCESSOR=armv7-a \ -DCMAKE_BUILD_TYPE=Release -DSFIZZ_JACK=OFF -DSFIZZ_LV2_UI=OFF .. -buildenv mod-plugin-builder make -j$(nproc) +buildenv mod-plugin-builder make -j2 diff --git a/.travis/script_plugins.sh b/.travis/script_plugins.sh index 66e6b09f..5f84a42c 100755 --- a/.travis/script_plugins.sh +++ b/.travis/script_plugins.sh @@ -11,4 +11,4 @@ cmake -DCMAKE_BUILD_TYPE=Release \ -DSFIZZ_STATIC_DEPENDENCIES=ON \ -DCMAKE_CXX_STANDARD=17 \ .. -make -j$(nproc) +make -j2 diff --git a/.travis/script_test.sh b/.travis/script_test.sh index 1a23a092..8481cbfb 100755 --- a/.travis/script_test.sh +++ b/.travis/script_test.sh @@ -10,5 +10,5 @@ cmake -DCMAKE_BUILD_TYPE=Release \ -DSFIZZ_LV2=OFF \ -DCMAKE_CXX_STANDARD=17 \ .. -make -j$(nproc) sfizz_tests +make -j2 sfizz_tests tests/sfizz_tests From 23f0ae6a13bcd2944f1965fbd6c935f9e9752835 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 13 Oct 2020 02:42:05 +0200 Subject: [PATCH 426/445] editor: add the next and previous file buttons --- editor/layout/main.fl | 32 ++++----- editor/src/editor/Editor.cpp | 115 ++++++++++++++++++++++++++++++ editor/src/editor/layout/main.hpp | 36 +++++----- 3 files changed, 149 insertions(+), 34 deletions(-) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index dffc20d7..fe3824c2 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -11,7 +11,7 @@ widget_class mainView {open class Background } Fl_Group {} { - comment {theme=darkTheme} + comment {theme=darkTheme} open xywh {0 0 800 110} class LogicalGroup } { @@ -44,16 +44,6 @@ widget_class mainView {open xywh {185 5 380 100} box ROUNDED_BOX class RoundedGroup } { - Fl_Box {} { - label {File:} - xywh {200 13 40 30} labelsize 16 - class Label - } - Fl_Box {} { - label {KS:} - xywh {200 45 40 30} labelsize 16 - class Label - } Fl_Box {} { label {Separator 1} xywh {195 41 360 5} box BORDER_BOX labeltype NO_LABEL @@ -66,12 +56,12 @@ widget_class mainView {open } Fl_Box sfzFileLabel_ { label {DefaultInstrument.sfz} - xywh {265 12 230 30} labelsize 20 + xywh {195 11 250 31} labelsize 20 align 20 class Label } Fl_Box {} { - label {Key switch} - xywh {265 44 230 30} labelsize 20 + label {Key switch:} + xywh {195 44 250 30} labelsize 20 align 20 class Label } Fl_Box {} { @@ -81,12 +71,12 @@ widget_class mainView {open } Fl_Button {} { comment {tag=kTagLoadSfzFile} - xywh {500 14 25 25} labelsize 24 + xywh {505 14 25 25} labelsize 24 class LoadFileButton } Fl_Button {} { comment {tag=kTagEditSfzFile} - xywh {525 14 25 25} labelsize 24 + xywh {530 14 25 25} labelsize 24 class EditFileButton } Fl_Box infoVoicesLabel_ { @@ -111,6 +101,16 @@ widget_class mainView {open xywh {500 76 50 25} labelsize 12 align 16 class Label } + Fl_Button {} { + comment {tag=kTagNextSfzFile} + xywh {480 14 25 25} labelsize 24 + class NextFileButton + } + Fl_Button {} { + comment {tag=kTagPreviousSfzFile} + xywh {455 14 25 25} labelsize 24 + class PreviousFileButton + } } Fl_Group {} {open xywh {570 5 225 100} box ROUNDED_BOX diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 4c16773d..37bee6a6 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -11,8 +11,13 @@ #include "NativeHelpers.h" #include #include +#include #include #include +#include +#include +#include +#include #include #include @@ -46,6 +51,8 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { enum { kTagLoadSfzFile, kTagEditSfzFile, + kTagPreviousSfzFile, + kTagNextSfzFile, kTagSetVolume, kTagSetNumVoices, kTagSetOversampling, @@ -101,9 +108,12 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { void chooseSfzFile(); void changeSfzFile(const std::string& filePath); + void changeToNextSfzFile(long offset); void chooseScalaFile(); void changeScalaFile(const std::string& filePath); + static bool scanDirectoryFiles(const fs::path& dirPath, std::function filter, std::vector& fileNames); + static absl::string_view simplifiedFileName(absl::string_view path, absl::string_view removedSuffix, absl::string_view ifEmpty); void updateSfzFileLabel(const std::string& filePath); @@ -377,6 +387,8 @@ void Editor::Impl::createFrameContents() typedef STextButton HomeButton; typedef STextButton SettingsButton; typedef STextButton EditFileButton; + typedef STextButton PreviousFileButton; + typedef STextButton NextFileButton; typedef SPiano Piano; auto createLogicalGroup = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { @@ -505,6 +517,12 @@ void Editor::Impl::createFrameContents() auto createLoadFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { return createGlyphButton(u8"\ue1a3", bounds, tag, fontsize); }; + auto createPreviousFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + return createGlyphButton(u8"\ue0d9", bounds, tag, fontsize); + }; + auto createNextFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { + return createGlyphButton(u8"\ue0da", bounds, tag, fontsize); + }; auto createPiano = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { SPiano* piano = new SPiano(bounds); return piano; @@ -669,6 +687,56 @@ void Editor::Impl::changeSfzFile(const std::string& filePath) updateSfzFileLabel(filePath); } +void Editor::Impl::changeToNextSfzFile(long offset) +{ + if (currentSfzFile_.empty()) + return; + + const fs::path filePath = fs::u8path(currentSfzFile_); + const fs::path dirPath = filePath.parent_path(); + + // extract file names of regular files from the sfz directory + std::vector fileNames; + fileNames.reserve(64); + + auto fileFilter = [](const fs::path &name) -> bool { + std::string ext = name.extension().u8string(); + absl::AsciiStrToLower(&ext); + return ext == ".sfz"; + }; + + if (!scanDirectoryFiles(dirPath, fileFilter, fileNames)) + return; + + // sort file names + const size_t size = fileNames.size(); + if (size == 0) + return; + + std::sort(fileNames.begin(), fileNames.end()); + + // find our current position in the file name list + size_t currentIndex = 0; + const fs::path currentFileName = filePath.filename(); + + while (currentIndex + 1 < size && fileNames[currentIndex] < currentFileName) + ++currentIndex; + + // advance to the next or previous item + typedef typename std::make_signed::type signed_size_t; + + size_t newIndex = static_cast(currentIndex) + offset; + if (static_cast(newIndex) < 0) + newIndex = static_cast(newIndex) % + static_cast(size) + size; + newIndex %= size; + + if (newIndex != currentIndex) { + const fs::path newFilePath = dirPath / fileNames[newIndex]; + changeSfzFile(newFilePath.u8string()); + } +} + void Editor::Impl::chooseScalaFile() { SharedPointer fs = owned(CNewFileSelector::create(frame_)); @@ -694,6 +762,39 @@ void Editor::Impl::changeScalaFile(const std::string& filePath) updateScalaFileLabel(filePath); } +bool Editor::Impl::scanDirectoryFiles(const fs::path& dirPath, std::function filter, std::vector& fileNames) +{ + std::error_code ec; + fs::directory_iterator it { dirPath, ec }; + + if (ec) + return false; + + fileNames.clear(); + + while (!ec && it != fs::directory_iterator()) { + const fs::directory_entry& ent = *it; + + std::error_code fileEc; + const fs::file_status status = ent.status(fileEc); + if (fileEc) + continue; + + if (status.type() == fs::file_type::regular) { + fs::path fileName = ent.path().filename(); + if (!filter || filter(fileName)) + fileNames.push_back(std::move(fileName)); + } + + it.increment(ec); + } + + if (ec) + return false; + + return true; +} + absl::string_view Editor::Impl::simplifiedFileName(absl::string_view path, absl::string_view removedSuffix, absl::string_view ifEmpty) { if (path.empty()) @@ -892,6 +993,20 @@ void Editor::Impl::valueChanged(CControl* ctl) openFileInExternalEditor(currentSfzFile_.c_str()); break; + case kTagPreviousSfzFile: + if (value != 1) + break; + + Call::later([this]() { changeToNextSfzFile(-1); }); + break; + + case kTagNextSfzFile: + if (value != 1) + break; + + Call::later([this]() { changeToNextSfzFile(+1); }); + break; + case kTagLoadScalaFile: if (value != 1) break; diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index af348d36..f7121624 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -18,37 +18,37 @@ SettingsButton* const view__7 = createSettingsButton(CRect(107, 69, 132, 94), kT view__3->addView(view__7); RoundedGroup* const view__8 = createRoundedGroup(CRect(185, 5, 565, 105), -1, "", kCenterText, 14); view__2->addView(view__8); -Label* const view__9 = createLabel(CRect(15, 8, 55, 38), -1, "File:", kCenterText, 16); +HLine* const view__9 = createHLine(CRect(10, 36, 370, 41), -1, "", kCenterText, 14); view__8->addView(view__9); -Label* const view__10 = createLabel(CRect(15, 40, 55, 70), -1, "KS:", kCenterText, 16); +HLine* const view__10 = createHLine(CRect(10, 68, 370, 73), -1, "", kCenterText, 14); view__8->addView(view__10); -HLine* const view__11 = createHLine(CRect(10, 36, 370, 41), -1, "", kCenterText, 14); +Label* const view__11 = createLabel(CRect(10, 6, 260, 37), -1, "DefaultInstrument.sfz", kLeftText, 20); +sfzFileLabel_ = view__11; view__8->addView(view__11); -HLine* const view__12 = createHLine(CRect(10, 68, 370, 73), -1, "", kCenterText, 14); +Label* const view__12 = createLabel(CRect(10, 39, 260, 69), -1, "Key switch:", kLeftText, 20); view__8->addView(view__12); -Label* const view__13 = createLabel(CRect(80, 7, 310, 37), -1, "DefaultInstrument.sfz", kCenterText, 20); -sfzFileLabel_ = view__13; +Label* const view__13 = createLabel(CRect(10, 71, 70, 96), -1, "Voices:", kRightText, 12); view__8->addView(view__13); -Label* const view__14 = createLabel(CRect(80, 39, 310, 69), -1, "Key switch", kCenterText, 20); +LoadFileButton* const view__14 = createLoadFileButton(CRect(320, 9, 345, 34), kTagLoadSfzFile, "", kCenterText, 24); view__8->addView(view__14); -Label* const view__15 = createLabel(CRect(10, 71, 70, 96), -1, "Voices:", kRightText, 12); +EditFileButton* const view__15 = createEditFileButton(CRect(345, 9, 370, 34), kTagEditSfzFile, "", kCenterText, 24); view__8->addView(view__15); -LoadFileButton* const view__16 = createLoadFileButton(CRect(315, 9, 340, 34), kTagLoadSfzFile, "", kCenterText, 24); +Label* const view__16 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); +infoVoicesLabel_ = view__16; view__8->addView(view__16); -EditFileButton* const view__17 = createEditFileButton(CRect(340, 9, 365, 34), kTagEditSfzFile, "", kCenterText, 24); +Label* const view__17 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); view__8->addView(view__17); -Label* const view__18 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); -infoVoicesLabel_ = view__18; +Label* const view__18 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); +numVoicesLabel_ = view__18; view__8->addView(view__18); -Label* const view__19 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); +Label* const view__19 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); view__8->addView(view__19); -Label* const view__20 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); -numVoicesLabel_ = view__20; +Label* const view__20 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); +memoryLabel_ = view__20; view__8->addView(view__20); -Label* const view__21 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); +NextFileButton* const view__21 = createNextFileButton(CRect(295, 9, 320, 34), kTagNextSfzFile, "", kCenterText, 24); view__8->addView(view__21); -Label* const view__22 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); -memoryLabel_ = view__22; +PreviousFileButton* const view__22 = createPreviousFileButton(CRect(270, 9, 295, 34), kTagPreviousSfzFile, "", kCenterText, 24); view__8->addView(view__22); RoundedGroup* const view__23 = createRoundedGroup(CRect(570, 5, 795, 105), -1, "", kCenterText, 14); view__2->addView(view__23); From 3f9d6de3635c130c716dc0eae9ef9891b949b57e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 13 Oct 2020 04:03:32 +0200 Subject: [PATCH 427/445] editor: update the icon highlight color [ci skip] --- editor/src/editor/Editor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 37bee6a6..142d5dcc 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -343,7 +343,7 @@ void Editor::Impl::createFrameContents() lightTheme.titleBoxText = { 0xff, 0xff, 0xff }; lightTheme.titleBoxBackground = { 0x2e, 0x34, 0x36 }; lightTheme.icon = lightTheme.text; - lightTheme.iconHighlight = { 0xa8, 0x62, 0x34 }; + lightTheme.iconHighlight = { 0xfd, 0x98, 0x00 }; lightTheme.valueText = { 0xff, 0xff, 0xff }; lightTheme.valueBackground = { 0x2e, 0x34, 0x36 }; lightTheme.knobActiveTrackColor = { 0x00, 0xb6, 0x2a }; @@ -355,7 +355,7 @@ void Editor::Impl::createFrameContents() darkTheme.titleBoxText = { 0x00, 0x00, 0x00 }; darkTheme.titleBoxBackground = { 0xba, 0xbd, 0xb6 }; darkTheme.icon = darkTheme.text; - darkTheme.iconHighlight = { 0xa8, 0x62, 0x34 }; + darkTheme.iconHighlight = { 0xfd, 0x98, 0x00 }; darkTheme.valueText = { 0x2e, 0x34, 0x36 }; darkTheme.valueBackground = { 0xff, 0xff, 0xff }; darkTheme.knobActiveTrackColor = { 0x00, 0xb6, 0x2a }; From d8c3e20d58ecaf25231d6718ee5b3424c23814e9 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 13 Oct 2020 04:07:39 +0200 Subject: [PATCH 428/445] editor: reorder UI components [ci skip] --- editor/layout/main.fl | 22 +++++++++++----------- editor/src/editor/layout/main.hpp | 24 ++++++++++++------------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index fe3824c2..ec6217c9 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -69,6 +69,16 @@ widget_class mainView {open xywh {195 76 60 25} labelsize 12 align 24 class Label } + Fl_Button {} { + comment {tag=kTagPreviousSfzFile} selected + xywh {455 14 25 25} labelsize 24 + class PreviousFileButton + } + Fl_Button {} { + comment {tag=kTagNextSfzFile} + xywh {480 14 25 25} labelsize 24 + class NextFileButton + } Fl_Button {} { comment {tag=kTagLoadSfzFile} xywh {505 14 25 25} labelsize 24 @@ -101,16 +111,6 @@ widget_class mainView {open xywh {500 76 50 25} labelsize 12 align 16 class Label } - Fl_Button {} { - comment {tag=kTagNextSfzFile} - xywh {480 14 25 25} labelsize 24 - class NextFileButton - } - Fl_Button {} { - comment {tag=kTagPreviousSfzFile} - xywh {455 14 25 25} labelsize 24 - class PreviousFileButton - } } Fl_Group {} {open xywh {570 5 225 100} box ROUNDED_BOX @@ -216,7 +216,7 @@ widget_class mainView {open } } } - Fl_Group {subPanels_[kPanelSettings]} {selected + Fl_Group {subPanels_[kPanelSettings]} { xywh {5 109 790 286} class LogicalGroup } { diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index f7121624..0a39b4a0 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -29,26 +29,26 @@ Label* const view__12 = createLabel(CRect(10, 39, 260, 69), -1, "Key switch:", k view__8->addView(view__12); Label* const view__13 = createLabel(CRect(10, 71, 70, 96), -1, "Voices:", kRightText, 12); view__8->addView(view__13); -LoadFileButton* const view__14 = createLoadFileButton(CRect(320, 9, 345, 34), kTagLoadSfzFile, "", kCenterText, 24); +PreviousFileButton* const view__14 = createPreviousFileButton(CRect(270, 9, 295, 34), kTagPreviousSfzFile, "", kCenterText, 24); view__8->addView(view__14); -EditFileButton* const view__15 = createEditFileButton(CRect(345, 9, 370, 34), kTagEditSfzFile, "", kCenterText, 24); +NextFileButton* const view__15 = createNextFileButton(CRect(295, 9, 320, 34), kTagNextSfzFile, "", kCenterText, 24); view__8->addView(view__15); -Label* const view__16 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); -infoVoicesLabel_ = view__16; +LoadFileButton* const view__16 = createLoadFileButton(CRect(320, 9, 345, 34), kTagLoadSfzFile, "", kCenterText, 24); view__8->addView(view__16); -Label* const view__17 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); +EditFileButton* const view__17 = createEditFileButton(CRect(345, 9, 370, 34), kTagEditSfzFile, "", kCenterText, 24); view__8->addView(view__17); -Label* const view__18 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); -numVoicesLabel_ = view__18; +Label* const view__18 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); +infoVoicesLabel_ = view__18; view__8->addView(view__18); -Label* const view__19 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); +Label* const view__19 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); view__8->addView(view__19); -Label* const view__20 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); -memoryLabel_ = view__20; +Label* const view__20 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); +numVoicesLabel_ = view__20; view__8->addView(view__20); -NextFileButton* const view__21 = createNextFileButton(CRect(295, 9, 320, 34), kTagNextSfzFile, "", kCenterText, 24); +Label* const view__21 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); view__8->addView(view__21); -PreviousFileButton* const view__22 = createPreviousFileButton(CRect(270, 9, 295, 34), kTagPreviousSfzFile, "", kCenterText, 24); +Label* const view__22 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); +memoryLabel_ = view__22; view__8->addView(view__22); RoundedGroup* const view__23 = createRoundedGroup(CRect(570, 5, 795, 105), -1, "", kCenterText, 14); view__2->addView(view__23); From c3e05b7b98b1501c7bf4b2b23a560d5a0bf0bc63 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 13 Oct 2020 13:38:54 +0200 Subject: [PATCH 429/445] editor: use a different icon for CC --- editor/src/editor/Editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 142d5dcc..d624e385 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -506,7 +506,7 @@ void Editor::Impl::createFrameContents() }; auto createCCButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { // return createGlyphButton(u8"\ue240", bounds, tag, fontsize); - return createGlyphButton(u8"\ue140", bounds, tag, fontsize); + return createGlyphButton(u8"\ue253", bounds, tag, fontsize); }; auto createSettingsButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { return createGlyphButton(u8"\ue2e4", bounds, tag, fontsize); From d1b0750c5b1c8d6ce9e52e132baf85aaba05844f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 13 Oct 2020 17:22:43 +0200 Subject: [PATCH 430/445] editor: file operations popup --- editor/layout/main.fl | 17 +-- editor/src/editor/Editor.cpp | 19 +++ editor/src/editor/GUIComponents.cpp | 124 ++++++++++++++++ editor/src/editor/GUIComponents.h | 50 ++++++- editor/src/editor/layout/main.hpp | 221 ++++++++++++++-------------- 5 files changed, 308 insertions(+), 123 deletions(-) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index ec6217c9..fd915c01 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -70,24 +70,19 @@ widget_class mainView {open class Label } Fl_Button {} { - comment {tag=kTagPreviousSfzFile} selected - xywh {455 14 25 25} labelsize 24 + comment {tag=kTagPreviousSfzFile} + xywh {480 14 25 25} labelsize 24 class PreviousFileButton } Fl_Button {} { comment {tag=kTagNextSfzFile} - xywh {480 14 25 25} labelsize 24 + xywh {505 14 25 25} labelsize 24 class NextFileButton } - Fl_Button {} { - comment {tag=kTagLoadSfzFile} - xywh {505 14 25 25} labelsize 24 - class LoadFileButton - } - Fl_Button {} { - comment {tag=kTagEditSfzFile} + Fl_Button fileOperationsMenu_ { + comment {tag=kTagFileOperations} selected xywh {530 14 25 25} labelsize 24 - class EditFileButton + class ChevronDropDown } Fl_Box infoVoicesLabel_ { xywh {260 76 50 25} labelsize 12 align 16 diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index d624e385..2443c768 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -53,6 +53,7 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { kTagEditSfzFile, kTagPreviousSfzFile, kTagNextSfzFile, + kTagFileOperations, kTagSetVolume, kTagSetNumVoices, kTagSetOversampling, @@ -93,6 +94,8 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { CTextLabel* memoryLabel_ = nullptr; + SActionMenu* fileOperationsMenu_ = nullptr; + void uiReceiveValue(EditId id, const EditValue& v) override; void createFrameContents(); @@ -390,6 +393,7 @@ void Editor::Impl::createFrameContents() typedef STextButton PreviousFileButton; typedef STextButton NextFileButton; typedef SPiano Piano; + typedef SActionMenu ChevronDropDown; auto createLogicalGroup = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { CViewContainer* container = new CViewContainer(bounds); @@ -527,6 +531,16 @@ void Editor::Impl::createFrameContents() SPiano* piano = new SPiano(bounds); return piano; }; + auto createChevronDropDown = [this, &theme](const CRect& bounds, int, const char*, CHoriTxtAlign, int fontsize) { + SActionMenu* menu = new SActionMenu(bounds, this); + menu->setTitle(u8"\ue0d7"); + menu->setFont(new CFontDesc("Sfizz Fluent System R20", fontsize)); + menu->setFontColor(theme->icon); + menu->setHoverColor(theme->iconHighlight); + menu->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + menu->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + return menu; + }; auto createBackground = [&background](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { CViewContainer* container = new CViewContainer(bounds); container->setBackground(background); @@ -647,6 +661,11 @@ void Editor::Impl::createFrameContents() return true; }); + if (SActionMenu* menu = fileOperationsMenu_) { + menu->addEntry("Load file", kTagLoadSfzFile); + menu->addEntry("Edit file", kTagEditSfzFile); + } + /// CViewContainer* panel; activePanel_ = 0; diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index abf52702..902231ee 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -429,6 +429,130 @@ void SValueMenu::onItemClicked(int32_t index) valueChanged(); } +/// +SActionMenu::SActionMenu(const CRect& bounds, IControlListener* listener) + : CParamDisplay(bounds), menuListener_(owned(new MenuListener(*this))) +{ + setListener(listener); + + auto toString = [](float, std::string& result, CParamDisplay* display) { + result = static_cast(display)->getTitle(); + return true; + }; + + setValueToStringFunction2(toString); +} + +void SActionMenu::setTitle(std::string title) +{ + title_ = std::move(title); + invalid(); +} + +void SActionMenu::setHoverColor(const CColor& color) +{ + hoverColor_ = color; +} + +CMenuItem* SActionMenu::addEntry(CMenuItem* item, int32_t tag, int32_t index) +{ + if (index < 0 || index > getNbEntries()) { + menuItems_.emplace_back(owned(item)); + menuItemTags_.emplace_back(tag); + } + else + { + menuItems_.insert(menuItems_.begin() + index, owned(item)); + menuItemTags_.insert(menuItemTags_.begin() + index, tag); + } + return item; +} + +CMenuItem* SActionMenu::addEntry(const UTF8String& title, int32_t tag, int32_t index, int32_t itemFlags) +{ + if (title == "-") + return addSeparator(index); + CMenuItem* item = new CMenuItem(title, nullptr, 0, nullptr, itemFlags); + return addEntry(item, tag, index); +} + +CMenuItem* SActionMenu::addSeparator(int32_t index) +{ + CMenuItem* item = new CMenuItem("", nullptr, 0, nullptr, CMenuItem::kSeparator); + return addEntry(item, 0.0f, index); +} + +int32_t SActionMenu::getNbEntries() const +{ + return static_cast(menuItems_.size()); +} + +void SActionMenu::draw(CDrawContext* dc) +{ + CColor backupColor = fontColor; + if (hovered_) + fontColor = hoverColor_; + CParamDisplay::draw(dc); + if (hovered_) + fontColor = backupColor; +} + +CMouseEventResult SActionMenu::onMouseEntered(CPoint& where, const CButtonState& buttons) +{ + hovered_ = true; + invalid(); + return CParamDisplay::onMouseEntered(where, buttons); +} + +CMouseEventResult SActionMenu::onMouseExited(CPoint& where, const CButtonState& buttons) +{ + hovered_ = false; + invalid(); + return CParamDisplay::onMouseExited(where, buttons); +} + +CMouseEventResult SActionMenu::onMouseDown(CPoint& where, const CButtonState& buttons) +{ + (void)where; + + if (buttons & (kLButton|kRButton|kApple)) { + CFrame* frame = getFrame(); + CRect bounds = getViewSize(); + + CPoint frameWhere = bounds.getBottomLeft(); + this->localToFrame(frameWhere); + + auto self = shared(this); + frame->doAfterEventProcessing([self, frameWhere]() { + if (CFrame* frame = self->getFrame()) { + SharedPointer menu = owned(new COptionMenu(CRect(), self->menuListener_, -1, nullptr, nullptr, COptionMenu::kPopupStyle)); + for (const SharedPointer& item : self->menuItems_) { + menu->addEntry(item); + item->remember(); // above call does not increment refcount + } + menu->setFont(self->getFont()); + menu->setFontColor(self->getFontColor()); + menu->setBackColor(self->getBackColor()); + menu->popup(frame, frameWhere + CPoint(0.0, 1.0)); + } + }); + return kMouseDownEventHandledButDontNeedMovedOrUpEvents; + } + + return kMouseEventNotHandled; +} + +void SActionMenu::onItemClicked(int32_t index) +{ + setTag(menuItemTags_[index]); + setValue(1.0f); + if (listener) + listener->valueChanged(this); + setValue(0.0f); + if (listener) + listener->valueChanged(this); +} + /// void STextButton::setHoverColor (const CColor& color) { diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index c9afccba..3679c781 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -122,7 +122,7 @@ public: int32_t getNbEntries() const; protected: - CMouseEventResult onMouseDown(CPoint& where, const CButtonState& buttons); + CMouseEventResult onMouseDown(CPoint& where, const CButtonState& buttons) override; private: class MenuListener; @@ -148,12 +148,60 @@ private: }; }; +/// +class SActionMenu : public CParamDisplay { +public: + explicit SActionMenu(const CRect& bounds, IControlListener* listener); + std::string getTitle() const { return title_; } + void setTitle(std::string title); + CColor getHoverColor() const { return hoverColor_; } + void setHoverColor(const CColor& color); + CMenuItem* addEntry(CMenuItem* item, int32_t tag, int32_t index = -1); + CMenuItem* addEntry(const UTF8String& title, int32_t tag, int32_t index = -1, int32_t itemFlags = CMenuItem::kNoFlags); + CMenuItem* addSeparator(int32_t index = -1); + int32_t getNbEntries() const; + +protected: + void draw(CDrawContext* dc) override; + CMouseEventResult onMouseEntered(CPoint& where, const CButtonState& buttons) override; + CMouseEventResult onMouseExited(CPoint& where, const CButtonState& buttons) override; + CMouseEventResult onMouseDown(CPoint& where, const CButtonState& buttons) override; + +private: + std::string title_; + CColor hoverColor_; + bool hovered_ = false; + + class MenuListener; + + // + void onItemClicked(int32_t index); + + // + CMenuItemList menuItems_; + std::vector menuItemTags_; + SharedPointer menuListener_; + + // + class MenuListener : public IControlListener, public NonAtomicReferenceCounted { + public: + explicit MenuListener(SActionMenu& menu) : menu_(menu) {} + void valueChanged(CControl* control) override + { + menu_.onItemClicked(static_cast(control->getValue())); + } + private: + SActionMenu& menu_; + }; +}; + /// class STextButton: public CTextButton { public: STextButton(const CRect& size, IControlListener* listener = nullptr, int32_t tag = -1, UTF8StringPtr title = nullptr) : CTextButton(size, listener, tag, title) {} + CColor getHoverColor() const { return hoverColor_; } void setHoverColor(const CColor& color); CMouseEventResult onMouseEntered (CPoint& where, const CButtonState& buttons) override; CMouseEventResult onMouseExited (CPoint& where, const CButtonState& buttons) override; diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index 0a39b4a0..a262d7c0 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -29,127 +29,126 @@ Label* const view__12 = createLabel(CRect(10, 39, 260, 69), -1, "Key switch:", k view__8->addView(view__12); Label* const view__13 = createLabel(CRect(10, 71, 70, 96), -1, "Voices:", kRightText, 12); view__8->addView(view__13); -PreviousFileButton* const view__14 = createPreviousFileButton(CRect(270, 9, 295, 34), kTagPreviousSfzFile, "", kCenterText, 24); +PreviousFileButton* const view__14 = createPreviousFileButton(CRect(295, 9, 320, 34), kTagPreviousSfzFile, "", kCenterText, 24); view__8->addView(view__14); -NextFileButton* const view__15 = createNextFileButton(CRect(295, 9, 320, 34), kTagNextSfzFile, "", kCenterText, 24); +NextFileButton* const view__15 = createNextFileButton(CRect(320, 9, 345, 34), kTagNextSfzFile, "", kCenterText, 24); view__8->addView(view__15); -LoadFileButton* const view__16 = createLoadFileButton(CRect(320, 9, 345, 34), kTagLoadSfzFile, "", kCenterText, 24); +ChevronDropDown* const view__16 = createChevronDropDown(CRect(345, 9, 370, 34), kTagFileOperations, "", kCenterText, 24); +fileOperationsMenu_ = view__16; view__8->addView(view__16); -EditFileButton* const view__17 = createEditFileButton(CRect(345, 9, 370, 34), kTagEditSfzFile, "", kCenterText, 24); +Label* const view__17 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); +infoVoicesLabel_ = view__17; view__8->addView(view__17); -Label* const view__18 = createLabel(CRect(75, 71, 125, 96), -1, "", kCenterText, 12); -infoVoicesLabel_ = view__18; +Label* const view__18 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); view__8->addView(view__18); -Label* const view__19 = createLabel(CRect(130, 71, 190, 96), -1, "Max:", kRightText, 12); +Label* const view__19 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); +numVoicesLabel_ = view__19; view__8->addView(view__19); -Label* const view__20 = createLabel(CRect(195, 71, 245, 96), -1, "", kCenterText, 12); -numVoicesLabel_ = view__20; +Label* const view__20 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); view__8->addView(view__20); -Label* const view__21 = createLabel(CRect(250, 71, 310, 96), -1, "Memory:", kRightText, 12); +Label* const view__21 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); +memoryLabel_ = view__21; view__8->addView(view__21); -Label* const view__22 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); -memoryLabel_ = view__22; -view__8->addView(view__22); -RoundedGroup* const view__23 = createRoundedGroup(CRect(570, 5, 795, 105), -1, "", kCenterText, 14); -view__2->addView(view__23); -Knob48* const view__24 = createKnob48(CRect(45, 15, 93, 63), -1, "", kCenterText, 14); -view__23->addView(view__24); +RoundedGroup* const view__22 = createRoundedGroup(CRect(570, 5, 795, 105), -1, "", kCenterText, 14); +view__2->addView(view__22); +Knob48* const view__23 = createKnob48(CRect(45, 15, 93, 63), -1, "", kCenterText, 14); +view__22->addView(view__23); +view__23->setVisible(false); +ValueLabel* const view__24 = createValueLabel(CRect(40, 65, 100, 70), -1, "Center", kCenterText, 12); +view__22->addView(view__24); view__24->setVisible(false); -ValueLabel* const view__25 = createValueLabel(CRect(40, 65, 100, 70), -1, "Center", kCenterText, 12); -view__23->addView(view__25); -view__25->setVisible(false); -StyledKnob* const view__26 = createStyledKnob(CRect(110, 15, 158, 63), kTagSetVolume, "", kCenterText, 14); -volumeSlider_ = view__26; -view__23->addView(view__26); -ValueLabel* const view__27 = createValueLabel(CRect(105, 65, 165, 87), -1, "0.0 dB", kCenterText, 12); -volumeLabel_ = view__27; -view__23->addView(view__27); -VMeter* const view__28 = createVMeter(CRect(175, 15, 210, 70), -1, "", kCenterText, 14); -view__23->addView(view__28); +StyledKnob* const view__25 = createStyledKnob(CRect(110, 15, 158, 63), kTagSetVolume, "", kCenterText, 14); +volumeSlider_ = view__25; +view__22->addView(view__25); +ValueLabel* const view__26 = createValueLabel(CRect(105, 65, 165, 87), -1, "0.0 dB", kCenterText, 12); +volumeLabel_ = view__26; +view__22->addView(view__26); +VMeter* const view__27 = createVMeter(CRect(175, 15, 210, 70), -1, "", kCenterText, 14); +view__22->addView(view__27); enterTheme(defaultTheme); -LogicalGroup* const view__29 = createLogicalGroup(CRect(5, 110, 796, 395), -1, "", kCenterText, 14); -subPanels_[kPanelGeneral] = view__29; -view__0->addView(view__29); -view__29->setVisible(false); -RoundedGroup* const view__30 = createRoundedGroup(CRect(0, 0, 175, 280), -1, "", kCenterText, 14); +LogicalGroup* const view__28 = createLogicalGroup(CRect(5, 110, 796, 395), -1, "", kCenterText, 14); +subPanels_[kPanelGeneral] = view__28; +view__0->addView(view__28); +view__28->setVisible(false); +RoundedGroup* const view__29 = createRoundedGroup(CRect(0, 0, 175, 280), -1, "", kCenterText, 14); +view__28->addView(view__29); +Label* const view__30 = createLabel(CRect(15, 10, 75, 35), -1, "Curves:", kLeftText, 14); view__29->addView(view__30); -Label* const view__31 = createLabel(CRect(15, 10, 75, 35), -1, "Curves:", kLeftText, 14); -view__30->addView(view__31); -Label* const view__32 = createLabel(CRect(15, 35, 75, 60), -1, "Masters:", kLeftText, 14); -view__30->addView(view__32); -Label* const view__33 = createLabel(CRect(15, 60, 75, 85), -1, "Groups:", kLeftText, 14); -view__30->addView(view__33); -Label* const view__34 = createLabel(CRect(15, 85, 75, 110), -1, "Regions:", kLeftText, 14); -view__30->addView(view__34); -Label* const view__35 = createLabel(CRect(15, 110, 75, 135), -1, "Samples:", kLeftText, 14); -view__30->addView(view__35); -Label* const view__36 = createLabel(CRect(115, 10, 155, 35), -1, "0", kCenterText, 14); -infoCurvesLabel_ = view__36; -view__30->addView(view__36); -Label* const view__37 = createLabel(CRect(115, 35, 155, 60), -1, "0", kCenterText, 14); -infoMastersLabel_ = view__37; -view__30->addView(view__37); -Label* const view__38 = createLabel(CRect(115, 60, 155, 85), -1, "0", kCenterText, 14); -infoGroupsLabel_ = view__38; -view__30->addView(view__38); -Label* const view__39 = createLabel(CRect(115, 85, 155, 110), -1, "0", kCenterText, 14); -infoRegionsLabel_ = view__39; -view__30->addView(view__39); -Label* const view__40 = createLabel(CRect(115, 110, 155, 135), -1, "0", kCenterText, 14); -infoSamplesLabel_ = view__40; -view__30->addView(view__40); -LogicalGroup* const view__41 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelControls] = view__41; -view__0->addView(view__41); -view__41->setVisible(false); -RoundedGroup* const view__42 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); +Label* const view__31 = createLabel(CRect(15, 35, 75, 60), -1, "Masters:", kLeftText, 14); +view__29->addView(view__31); +Label* const view__32 = createLabel(CRect(15, 60, 75, 85), -1, "Groups:", kLeftText, 14); +view__29->addView(view__32); +Label* const view__33 = createLabel(CRect(15, 85, 75, 110), -1, "Regions:", kLeftText, 14); +view__29->addView(view__33); +Label* const view__34 = createLabel(CRect(15, 110, 75, 135), -1, "Samples:", kLeftText, 14); +view__29->addView(view__34); +Label* const view__35 = createLabel(CRect(115, 10, 155, 35), -1, "0", kCenterText, 14); +infoCurvesLabel_ = view__35; +view__29->addView(view__35); +Label* const view__36 = createLabel(CRect(115, 35, 155, 60), -1, "0", kCenterText, 14); +infoMastersLabel_ = view__36; +view__29->addView(view__36); +Label* const view__37 = createLabel(CRect(115, 60, 155, 85), -1, "0", kCenterText, 14); +infoGroupsLabel_ = view__37; +view__29->addView(view__37); +Label* const view__38 = createLabel(CRect(115, 85, 155, 110), -1, "0", kCenterText, 14); +infoRegionsLabel_ = view__38; +view__29->addView(view__38); +Label* const view__39 = createLabel(CRect(115, 110, 155, 135), -1, "0", kCenterText, 14); +infoSamplesLabel_ = view__39; +view__29->addView(view__39); +LogicalGroup* const view__40 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelControls] = view__40; +view__0->addView(view__40); +view__40->setVisible(false); +RoundedGroup* const view__41 = createRoundedGroup(CRect(0, 0, 790, 285), -1, "", kCenterText, 14); +view__40->addView(view__41); +Label* const view__42 = createLabel(CRect(0, 0, 790, 285), -1, "Controls not available", kCenterText, 40); view__41->addView(view__42); -Label* const view__43 = createLabel(CRect(0, 0, 790, 285), -1, "Controls not available", kCenterText, 40); -view__42->addView(view__43); -LogicalGroup* const view__44 = createLogicalGroup(CRect(5, 109, 795, 395), -1, "", kCenterText, 14); -subPanels_[kPanelSettings] = view__44; -view__0->addView(view__44); -TitleGroup* const view__45 = createTitleGroup(CRect(255, 26, 535, 126), -1, "Engine", kCenterText, 12); +LogicalGroup* const view__43 = createLogicalGroup(CRect(5, 109, 795, 395), -1, "", kCenterText, 14); +subPanels_[kPanelSettings] = view__43; +view__0->addView(view__43); +TitleGroup* const view__44 = createTitleGroup(CRect(255, 26, 535, 126), -1, "Engine", kCenterText, 12); +view__43->addView(view__44); +ValueMenu* const view__45 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); +numVoicesSlider_ = view__45; view__44->addView(view__45); -ValueMenu* const view__46 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); -numVoicesSlider_ = view__46; -view__45->addView(view__46); -ValueLabel* const view__47 = createValueLabel(CRect(15, 20, 95, 45), -1, "Polyphony", kCenterText, 12); -view__45->addView(view__47); -ValueMenu* const view__48 = createValueMenu(CRect(110, 60, 170, 85), kTagSetOversampling, "", kCenterText, 12); -oversamplingSlider_ = view__48; -view__45->addView(view__48); -ValueLabel* const view__49 = createValueLabel(CRect(100, 20, 180, 45), -1, "Oversampling", kCenterText, 12); -view__45->addView(view__49); -ValueLabel* const view__50 = createValueLabel(CRect(185, 20, 265, 45), -1, "Preload size", kCenterText, 12); -view__45->addView(view__50); -ValueMenu* const view__51 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); -preloadSizeSlider_ = view__51; -view__45->addView(view__51); -TitleGroup* const view__52 = createTitleGroup(CRect(200, 161, 590, 261), -1, "Tuning", kCenterText, 12); -view__44->addView(view__52); -ValueLabel* const view__53 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); -view__52->addView(view__53); -ValueMenu* const view__54 = createValueMenu(CRect(220, 60, 280, 85), kTagSetTuningFrequency, "", kCenterText, 12); -tuningFrequencySlider_ = view__54; -view__52->addView(view__54); -ValueLabel* const view__55 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); -view__52->addView(view__55); -StyledKnob* const view__56 = createStyledKnob(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); -stretchedTuningSlider_ = view__56; -view__52->addView(view__56); -ValueLabel* const view__57 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); -view__52->addView(view__57); -ValueLabel* const view__58 = createValueLabel(CRect(20, 20, 120, 45), -1, "Scala file", kCenterText, 12); -view__52->addView(view__58); -ValueButton* const view__59 = createValueButton(CRect(20, 60, 120, 85), kTagLoadScalaFile, "DefaultScale", kCenterText, 12); -scalaFileButton_ = view__59; -view__52->addView(view__59); -ValueMenu* const view__60 = createValueMenu(CRect(135, 60, 170, 85), kTagSetScalaRootKey, "", kCenterText, 12); -scalaRootKeySlider_ = view__60; -view__52->addView(view__60); -ValueMenu* const view__61 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); -scalaRootOctaveSlider_ = view__61; -view__52->addView(view__61); -Piano* const view__62 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); -view__0->addView(view__62); +ValueLabel* const view__46 = createValueLabel(CRect(15, 20, 95, 45), -1, "Polyphony", kCenterText, 12); +view__44->addView(view__46); +ValueMenu* const view__47 = createValueMenu(CRect(110, 60, 170, 85), kTagSetOversampling, "", kCenterText, 12); +oversamplingSlider_ = view__47; +view__44->addView(view__47); +ValueLabel* const view__48 = createValueLabel(CRect(100, 20, 180, 45), -1, "Oversampling", kCenterText, 12); +view__44->addView(view__48); +ValueLabel* const view__49 = createValueLabel(CRect(185, 20, 265, 45), -1, "Preload size", kCenterText, 12); +view__44->addView(view__49); +ValueMenu* const view__50 = createValueMenu(CRect(195, 60, 255, 85), kTagSetPreloadSize, "", kCenterText, 12); +preloadSizeSlider_ = view__50; +view__44->addView(view__50); +TitleGroup* const view__51 = createTitleGroup(CRect(200, 161, 590, 261), -1, "Tuning", kCenterText, 12); +view__43->addView(view__51); +ValueLabel* const view__52 = createValueLabel(CRect(125, 20, 205, 45), -1, "Root key", kCenterText, 12); +view__51->addView(view__52); +ValueMenu* const view__53 = createValueMenu(CRect(220, 60, 280, 85), kTagSetTuningFrequency, "", kCenterText, 12); +tuningFrequencySlider_ = view__53; +view__51->addView(view__53); +ValueLabel* const view__54 = createValueLabel(CRect(210, 20, 290, 45), -1, "Frequency", kCenterText, 12); +view__51->addView(view__54); +StyledKnob* const view__55 = createStyledKnob(CRect(310, 45, 358, 93), kTagSetStretchedTuning, "", kCenterText, 14); +stretchedTuningSlider_ = view__55; +view__51->addView(view__55); +ValueLabel* const view__56 = createValueLabel(CRect(295, 20, 375, 45), -1, "Stretch", kCenterText, 12); +view__51->addView(view__56); +ValueLabel* const view__57 = createValueLabel(CRect(20, 20, 120, 45), -1, "Scala file", kCenterText, 12); +view__51->addView(view__57); +ValueButton* const view__58 = createValueButton(CRect(20, 60, 120, 85), kTagLoadScalaFile, "DefaultScale", kCenterText, 12); +scalaFileButton_ = view__58; +view__51->addView(view__58); +ValueMenu* const view__59 = createValueMenu(CRect(135, 60, 170, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootKeySlider_ = view__59; +view__51->addView(view__59); +ValueMenu* const view__60 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); +scalaRootOctaveSlider_ = view__60; +view__51->addView(view__60); +Piano* const view__61 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); +view__0->addView(view__61); From 5cd6a514aebfa1ca75926ab24a2b209c2a8aec64 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 13 Oct 2020 22:10:09 +0200 Subject: [PATCH 431/445] Allow the SFZ label to be clicked like a button --- editor/layout/main.fl | 5 +-- editor/src/editor/Editor.cpp | 52 ++++++++++++++++++++++++----- editor/src/editor/GUIComponents.cpp | 28 ++++++++++++---- editor/src/editor/GUIComponents.h | 8 ++++- editor/src/editor/layout/main.hpp | 2 +- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/editor/layout/main.fl b/editor/layout/main.fl index fd915c01..602c1d05 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -56,8 +56,9 @@ widget_class mainView {open } Fl_Box sfzFileLabel_ { label {DefaultInstrument.sfz} + comment {tag=kTagLoadSfzFile} selected xywh {195 11 250 31} labelsize 20 align 20 - class Label + class ClickableLabel } Fl_Box {} { label {Key switch:} @@ -80,7 +81,7 @@ widget_class mainView {open class NextFileButton } Fl_Button fileOperationsMenu_ { - comment {tag=kTagFileOperations} selected + comment {tag=kTagFileOperations} xywh {530 14 25 25} labelsize 24 class ChevronDropDown } diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 2443c768..cb61ad38 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -66,9 +66,9 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { kTagLastChangePanel = kTagFirstChangePanel + kNumPanels - 1, }; - CTextLabel* sfzFileLabel_ = nullptr; + STextButton* sfzFileLabel_ = nullptr; CTextLabel* scalaFileLabel_ = nullptr; - CTextButton* scalaFileButton_ = nullptr; + STextButton* scalaFileButton_ = nullptr; CControl *volumeSlider_ = nullptr; CTextLabel* volumeLabel_ = nullptr; SValueMenu *numVoicesSlider_ = nullptr; @@ -122,7 +122,8 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { void updateSfzFileLabel(const std::string& filePath); void updateScalaFileLabel(const std::string& filePath); static void updateLabelWithFileName(CTextLabel* label, const std::string& filePath, absl::string_view removedSuffix); - static void updateButtonWithFileName(CTextButton* button, const std::string& filePath, absl::string_view removedSuffix); + static void updateButtonWithFileName(STextButton* button, const std::string& filePath, absl::string_view removedSuffix); + static void updateSButtonWithFileName(STextButton* button, const std::string& filePath, absl::string_view removedSuffix); void updateVolumeLabel(float volume); void updateNumVoicesLabel(int numVoices); void updateOversamplingLabel(int oversamplingLog2); @@ -329,6 +330,8 @@ void Editor::Impl::createFrameContents() struct Theme { CColor boxBackground; CColor text; + CColor inactiveText; + CColor highlightedText; CColor titleBoxText; CColor titleBoxBackground; CColor icon; @@ -343,6 +346,8 @@ void Editor::Impl::createFrameContents() Theme lightTheme; lightTheme.boxBackground = { 0xba, 0xbd, 0xb6 }; lightTheme.text = { 0x00, 0x00, 0x00 }; + lightTheme.inactiveText = { 0xb2, 0xb2, 0xb2 }; + lightTheme.highlightedText = { 0xfd, 0x98, 0x00 }; lightTheme.titleBoxText = { 0xff, 0xff, 0xff }; lightTheme.titleBoxBackground = { 0x2e, 0x34, 0x36 }; lightTheme.icon = lightTheme.text; @@ -355,6 +360,8 @@ void Editor::Impl::createFrameContents() Theme darkTheme; darkTheme.boxBackground = { 0x2e, 0x34, 0x36 }; darkTheme.text = { 0xff, 0xff, 0xff }; + darkTheme.inactiveText = { 0xb2, 0xb2, 0xb2 }; + darkTheme.highlightedText = { 0xfd, 0x98, 0x00 }; darkTheme.titleBoxText = { 0x00, 0x00, 0x00 }; darkTheme.titleBoxBackground = { 0xba, 0xbd, 0xb6 }; darkTheme.icon = darkTheme.text; @@ -384,7 +391,8 @@ void Editor::Impl::createFrameContents() #if 0 typedef CTextButton Button; #endif - typedef CTextButton ValueButton; + typedef STextButton ClickableLabel; + typedef STextButton ValueButton; typedef STextButton LoadFileButton; typedef STextButton CCButton; typedef STextButton HomeButton; @@ -471,13 +479,31 @@ void Editor::Impl::createFrameContents() return button; }; #endif + auto createClickableLabel = [this, &theme](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { + STextButton* button = new STextButton(bounds, this, tag, label); + auto font = owned(new CFontDesc("Roboto", fontsize)); + button->setFont(font); + button->setTextAlignment(align); + button->setTextColor(theme->text); + button->setInactiveColor(theme->inactiveText); + button->setHoverColor(theme->highlightedText); + button->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + button->setFrameColorHighlighted(CColor(0x00, 0x00, 0x00, 0x00)); + SharedPointer gradient = owned(CGradient::create(0.0, 1.0, CColor(0x00, 0x00, 0x00, 0x00), CColor(0x00, 0x00, 0x00, 0x00))); + button->setGradient(gradient); + button->setGradientHighlighted(gradient); + return button; + }; auto createValueButton = [this, &theme](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { - CTextButton* button = new CTextButton(bounds, this, tag, label); + STextButton* button = new STextButton(bounds, this, tag, label); auto font = owned(new CFontDesc("Roboto", fontsize)); button->setFont(font); button->setTextAlignment(align); button->setTextColor(theme->valueText); + button->setInactiveColor(theme->inactiveText); + button->setHoverColor(theme->highlightedText); button->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + button->setFrameColorHighlighted(CColor(0x00, 0x00, 0x00, 0x00)); SharedPointer gradient = owned(CGradient::create(0.0, 1.0, theme->valueBackground, theme->valueBackground)); button->setGradient(gradient); button->setGradientHighlighted(gradient); @@ -501,6 +527,7 @@ void Editor::Impl::createFrameContents() btn->setTextColor(theme->icon); btn->setHoverColor(theme->iconHighlight); btn->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + btn->setFrameColorHighlighted(CColor(0x00, 0x00, 0x00, 0x00)); btn->setGradient(nullptr); btn->setGradientHighlighted(nullptr); return btn; @@ -834,7 +861,7 @@ absl::string_view Editor::Impl::simplifiedFileName(absl::string_view path, absl: void Editor::Impl::updateSfzFileLabel(const std::string& filePath) { - updateLabelWithFileName(sfzFileLabel_, filePath, ".sfz"); + updateButtonWithFileName(sfzFileLabel_, filePath, ".sfz"); } void Editor::Impl::updateScalaFileLabel(const std::string& filePath) @@ -852,13 +879,20 @@ void Editor::Impl::updateLabelWithFileName(CTextLabel* label, const std::string& label->setText(fileName.c_str()); } -void Editor::Impl::updateButtonWithFileName(CTextButton* button, const std::string& filePath, absl::string_view removedSuffix) +void Editor::Impl::updateButtonWithFileName(STextButton* button, const std::string& filePath, absl::string_view removedSuffix) { if (!button) return; - std::string fileName = std::string(simplifiedFileName(filePath, removedSuffix, "")); - button->setTitle(fileName.c_str()); + std::string fileName = std::string(simplifiedFileName(filePath, removedSuffix, {})); + if (!fileName.empty()) { + button->setTitle(fileName.c_str()); + button->setInactive(false); + } + else { + button->setTitle("No file"); + button->setInactive(true); + } } void Editor::Impl::updateVolumeLabel(float volume) diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index 902231ee..6a0fb92b 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -452,6 +452,7 @@ void SActionMenu::setTitle(std::string title) void SActionMenu::setHoverColor(const CColor& color) { hoverColor_ = color; + invalid(); } CMenuItem* SActionMenu::addEntry(CMenuItem* item, int32_t tag, int32_t index) @@ -554,33 +555,46 @@ void SActionMenu::onItemClicked(int32_t index) } /// -void STextButton::setHoverColor (const CColor& color) +void STextButton::setHoverColor(const CColor& color) { hoverColor_ = color; + invalid(); +} + +void STextButton::setInactiveColor(const CColor& color) +{ + inactiveColor_ = color; + invalid(); +} + +void STextButton::setInactive(bool b) +{ + inactive_ = b; + invalid(); } void STextButton::draw(CDrawContext* context) { CColor backupColor = textColor; - if (hovered) { + if (hovered_) textColor = hoverColor_; // textColor is protected - } + else if (inactive_) + textColor = inactiveColor_; CTextButton::draw(context); - if (hovered) - textColor = backupColor; + textColor = backupColor; } CMouseEventResult STextButton::onMouseEntered (CPoint& where, const CButtonState& buttons) { - hovered = true; + hovered_ = true; invalid(); return CTextButton::onMouseEntered(where, buttons); } CMouseEventResult STextButton::onMouseExited (CPoint& where, const CButtonState& buttons) { - hovered = false; + hovered_ = false; invalid(); return CTextButton::onMouseExited(where, buttons); } diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index 3679c781..eab40417 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -203,12 +203,18 @@ public: CColor getHoverColor() const { return hoverColor_; } void setHoverColor(const CColor& color); + CColor getInactiveColor() const { return inactiveColor_; } + void setInactiveColor(const CColor& color); + bool isInactive() const { return inactive_; } + void setInactive(bool b); CMouseEventResult onMouseEntered (CPoint& where, const CButtonState& buttons) override; CMouseEventResult onMouseExited (CPoint& where, const CButtonState& buttons) override; void draw(CDrawContext* context) override; private: CColor hoverColor_; - bool hovered { false }; + bool hovered_ { false }; + CColor inactiveColor_; + bool inactive_ { false }; }; /// diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index a262d7c0..c93cf64e 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -22,7 +22,7 @@ HLine* const view__9 = createHLine(CRect(10, 36, 370, 41), -1, "", kCenterText, view__8->addView(view__9); HLine* const view__10 = createHLine(CRect(10, 68, 370, 73), -1, "", kCenterText, 14); view__8->addView(view__10); -Label* const view__11 = createLabel(CRect(10, 6, 260, 37), -1, "DefaultInstrument.sfz", kLeftText, 20); +ClickableLabel* const view__11 = createClickableLabel(CRect(10, 6, 260, 37), kTagLoadSfzFile, "DefaultInstrument.sfz", kLeftText, 20); sfzFileLabel_ = view__11; view__8->addView(view__11); Label* const view__12 = createLabel(CRect(10, 39, 260, 69), -1, "Key switch:", kLeftText, 20); From 39a4df017e758d01348176749a835fec082e0181 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 14 Oct 2020 15:29:19 +0200 Subject: [PATCH 432/445] Update the piano --- editor/CMakeLists.txt | 2 + editor/layout/main.fl | 12 +- editor/src/editor/Editor.cpp | 24 ++- editor/src/editor/GUIComponents.cpp | 195 ----------------------- editor/src/editor/GUIComponents.h | 30 ---- editor/src/editor/GUIPiano.cpp | 237 ++++++++++++++++++++++++++++ editor/src/editor/GUIPiano.h | 72 +++++++++ editor/src/editor/layout/main.hpp | 5 +- 8 files changed, 343 insertions(+), 234 deletions(-) create mode 100644 editor/src/editor/GUIPiano.cpp create mode 100644 editor/src/editor/GUIPiano.h diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 952a0e98..8f676d7e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -34,6 +34,8 @@ add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/EditorController.h src/editor/GUIComponents.h src/editor/GUIComponents.cpp + src/editor/GUIPiano.h + src/editor/GUIPiano.cpp src/editor/NativeHelpers.h src/editor/NativeHelpers.cpp src/editor/layout/main.hpp diff --git a/editor/layout/main.fl b/editor/layout/main.fl index 602c1d05..59d22a06 100644 --- a/editor/layout/main.fl +++ b/editor/layout/main.fl @@ -56,7 +56,7 @@ widget_class mainView {open } Fl_Box sfzFileLabel_ { label {DefaultInstrument.sfz} - comment {tag=kTagLoadSfzFile} selected + comment {tag=kTagLoadSfzFile} xywh {195 11 250 31} labelsize 20 align 20 class ClickableLabel } @@ -138,7 +138,7 @@ widget_class mainView {open } } Fl_Group {subPanels_[kPanelGeneral]} { - xywh {5 110 791 285} hide + xywh {5 110 791 285} class LogicalGroup } { Fl_Group {} {open @@ -212,8 +212,8 @@ widget_class mainView {open } } } - Fl_Group {subPanels_[kPanelSettings]} { - xywh {5 109 790 286} + Fl_Group {subPanels_[kPanelSettings]} {open + xywh {5 109 790 286} hide class LogicalGroup } { Fl_Group {} { @@ -305,8 +305,8 @@ widget_class mainView {open } } } - Fl_Box {} { - xywh {5 400 790 70} + Fl_Box piano_ {selected + xywh {5 400 790 70} labelsize 12 class Piano } } diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index cb61ad38..dc4870b2 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -8,6 +8,7 @@ #include "EditorController.h" #include "EditIds.h" #include "GUIComponents.h" +#include "GUIPiano.h" #include "NativeHelpers.h" #include #include @@ -96,6 +97,8 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { SActionMenu* fileOperationsMenu_ = nullptr; + SPiano* piano_ = nullptr; + void uiReceiveValue(EditId id, const EditValue& v) override; void createFrameContents(); @@ -554,8 +557,10 @@ void Editor::Impl::createFrameContents() auto createNextFileButton = [&createGlyphButton](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int fontsize) { return createGlyphButton(u8"\ue0da", bounds, tag, fontsize); }; - auto createPiano = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + auto createPiano = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int fontsize) { SPiano* piano = new SPiano(bounds); + auto font = owned(new CFontDesc("Roboto", fontsize)); + piano->setFont(font); return piano; }; auto createChevronDropDown = [this, &theme](const CRect& bounds, int, const char*, CHoriTxtAlign, int fontsize) { @@ -693,6 +698,23 @@ void Editor::Impl::createFrameContents() menu->addEntry("Edit file", kTagEditSfzFile); } + if (SPiano* piano = piano_) { + piano->onKeyPressed = [this](unsigned key, float vel) { + uint8_t msg[3]; + msg[0] = 0x90; + msg[1] = static_cast(key); + msg[2] = static_cast(std::max(1, static_cast(vel * 127))); + ctrl_->uiSendMIDI(msg, sizeof(msg)); + }; + piano->onKeyReleased = [this](unsigned key, float vel) { + uint8_t msg[3]; + msg[0] = 0x80; + msg[1] = static_cast(key); + msg[2] = static_cast(vel * 127); + ctrl_->uiSendMIDI(msg, sizeof(msg)); + }; + } + /// CViewContainer* panel; activePanel_ = 0; diff --git a/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp index 6a0fb92b..803698dd 100644 --- a/editor/src/editor/GUIComponents.cpp +++ b/editor/src/editor/GUIComponents.cpp @@ -154,201 +154,6 @@ bool SFileDropTarget::isFileDrop(IDataPackage* package) package->getDataType(0) == IDataPackage::kFilePath; } -/// -SPiano::SPiano(const CRect& bounds) - : CView(bounds), font_(kNormalFont) -{ -} - -void SPiano::setFont(CFontRef font) -{ - font_ = font; - invalid(); -} - -void SPiano::clearKeyRanges() -{ - keyInRange_.reset(); -} - -void SPiano::addKeyRange(int start, int end) -{ - start = std::min(127, std::max(0, start)); - end = std::min(127, std::max(0, end)); - - for (int x = start; x <= end; ++x) - keyInRange_.set(x); -} - -CCoord SPiano::getKeyWidth() -{ - return 6.0; -} - -CCoord SPiano::getKeySwitchesHeight() -{ - return 20.0; -} - -CCoord SPiano::getKeyRangesHeight() -{ - return 11.0; -} - -CCoord SPiano::getKeysHeight() const -{ - return getHeight() - - (getKeySwitchesHeight() + getKeyRangesHeight() + getOctavesHeight()); -} - -CCoord SPiano::getOctavesHeight() const -{ - return font_->getSize(); -} - -void SPiano::getZoneDimensions( - CRect* pKeySwitches, - CRect* pKeyboard, - CRect* pKeyRanges, - CRect* pOctaves) -{ - CRect bounds = getViewSize(); - - CRect keySwitches(bounds); - keySwitches.setHeight(getKeySwitchesHeight()); - - CRect keyboard(bounds); - keyboard.top = keySwitches.bottom; - keyboard.setHeight(getKeysHeight()); - - CRect keyRanges(bounds); - keyRanges.top = keyboard.bottom; - keyRanges.setHeight(getKeyRangesHeight()); - - CRect octaves(bounds); - octaves.top = keyRanges.bottom; - octaves.setHeight(getOctavesHeight()); - - // apply some paddings - keySwitches.extend(-2.0, -2.0); - keyboard.extend(-2.0, -2.0); - keyRanges.extend(-2.0, -4.0); - octaves.extend(-2.0, -2.0); - - // offsets for centered keyboard - CCoord keyWidth = getKeyWidth(); - CCoord offset = std::round((keyboard.getWidth() - (128.0 * keyWidth)) * 0.5); - if (offset > 0) { - keySwitches.extend(-offset, 0.0); - keyboard.extend(-offset, 0.0); - keyRanges.extend(-offset, 0.0); - octaves.extend(-offset, 0.0); - } - - // - if (pKeySwitches) - *pKeySwitches = keySwitches; - if (pKeyboard) - *pKeyboard = keyboard; - if (pKeyRanges) - *pKeyRanges = keyRanges; - if (pOctaves) - *pOctaves = octaves; -} - -void SPiano::draw(CDrawContext* dc) -{ - CRect bounds = getViewSize(); - - dc->setDrawMode(kAntiAliasing); - - SharedPointer path; - - path = owned(dc->createGraphicsPath()); - path->addRoundRect(bounds, 5.0); - dc->setFillColor(CColor(0xca, 0xca, 0xca)); - dc->drawGraphicsPath(path, CDrawContext::kPathFilled); - - // - CRect rectKeySwitches; - CRect rectKeyboard; - CRect rectKeyRanges; - CRect rectOctaves; - getZoneDimensions(&rectKeySwitches, &rectKeyboard, &rectKeyRanges, &rectOctaves); - - // - path = owned(dc->createGraphicsPath()); - path->addRoundRect(rectKeyboard, 1.0); - dc->setFillColor(CColor(0xff, 0xff, 0xff)); - dc->drawGraphicsPath(path, CDrawContext::kPathFilled); - - CCoord keyWidth = getKeyWidth(); - for (int key = 0; key < 128; ++key) { - CCoord keyX = rectKeyboard.left + key * keyWidth; - int key12 = key % 12; - if (key12 == 1 || key12 == 3 || - key12 == 6 || key12 == 8 || key12 == 10) - { - CRect blackRect(keyX, rectKeyboard.top + 2, keyX + keyWidth, rectKeyboard.bottom - 2); - path = owned(dc->createGraphicsPath()); - path->addRoundRect(blackRect, 1.0); - dc->setFillColor(CColor(0x02, 0x02, 0x02)); - dc->drawGraphicsPath(path, CDrawContext::kPathFilled); - } - if (key != 0 && key12 == 0) { - dc->setLineWidth(1.5); - dc->setFrameColor(CColor(0x63, 0x63, 0x63)); - dc->drawLine(CPoint(keyX, rectKeyboard.top), CPoint(keyX, rectKeyboard.bottom)); - } - if (key12 == 5) { - CCoord pad = rectKeyboard.getHeight() * 0.4; - dc->setLineWidth(1.0); - dc->setFrameColor(CColor(0x63, 0x63, 0x63)); - dc->drawLine(CPoint(keyX, rectKeyboard.top + pad), CPoint(keyX, rectKeyboard.bottom - pad)); - } - } - - // - - for (int rangeStart = 0; rangeStart < 128;) - { - if (!keyInRange_[rangeStart]) { - ++rangeStart; - } - else { - int rangeEnd = rangeStart; - while (rangeEnd + 1 < 128 && keyInRange_[rangeEnd + 1]) - ++rangeEnd; - - CCoord rangeStartX = rectKeyRanges.left + rangeStart * keyWidth; - CCoord rangeEndX = rectKeyRanges.left + (rangeEnd + 1.0) * keyWidth; - CRect rectRange(rangeStartX, rectKeyRanges.top, rangeEndX, rectKeyRanges.bottom); - - path = owned(dc->createGraphicsPath()); - path->addRoundRect(rectRange, 2.0); - dc->setFillColor(CColor(0x0f, 0x0f, 0x0f)); - dc->drawGraphicsPath(path, CDrawContext::kPathFilled); - - rangeStart = rangeEnd + 1; - } - } - - // - - for (int key = 0; key < 128; ++key) { - CCoord keyX = rectOctaves.left + key * keyWidth; - int key12 = key % 12; - if (key12 == 0) { - CRect textRect(keyX, rectOctaves.top, keyX + 12 * keyWidth, rectOctaves.bottom); - dc->setFont(font_); - dc->setFontColor(CColor(0x63, 0x63, 0x63)); - dc->drawString(std::to_string(key / 12 - 1).c_str(), textRect, kLeftText); - } - } - - // -} - /// SValueMenu::SValueMenu(const CRect& bounds, IControlListener* listener, int32_t tag) : CParamDisplay(bounds), menuListener_(owned(new MenuListener(*this))) diff --git a/editor/src/editor/GUIComponents.h b/editor/src/editor/GUIComponents.h index eab40417..5a807b98 100644 --- a/editor/src/editor/GUIComponents.h +++ b/editor/src/editor/GUIComponents.h @@ -82,36 +82,6 @@ private: FileDropFunction dropFunction_; }; -/// -class SPiano : public CView { -public: - explicit SPiano(const CRect& bounds); - CFontRef getFont() const { return font_; } - void setFont(CFontRef font); - - void clearKeyRanges(); - void addKeyRange(int start, int end); - -protected: - static CCoord getKeyWidth(); - static CCoord getKeySwitchesHeight(); - static CCoord getKeyRangesHeight(); - CCoord getKeysHeight() const; - CCoord getOctavesHeight() const; - - void getZoneDimensions( - CRect* pKeySwitches, - CRect* pKeyboard, - CRect* pKeyRanges, - CRect* pOctaves); - - void draw(CDrawContext* dc) override; - -private: - SharedPointer font_; - std::bitset<128> keyInRange_; -}; - /// class SValueMenu : public CParamDisplay { public: diff --git a/editor/src/editor/GUIPiano.cpp b/editor/src/editor/GUIPiano.cpp new file mode 100644 index 00000000..6d4919bb --- /dev/null +++ b/editor/src/editor/GUIPiano.cpp @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "GUIPiano.h" +#include "utility/vstgui_before.h" +#include "vstgui/lib/cdrawcontext.h" +#include "vstgui/lib/cgraphicspath.h" +#include "utility/vstgui_after.h" +#include + +static constexpr CCoord keyoffs[12] = {0, 0.6, 1, 1.8, 2, 3, + 3.55, 4, 4.7, 5, 5.85, 6}; +static constexpr bool black[12] = {0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0}; + +SPiano::SPiano(CRect bounds) + : CView(bounds) +{ + setNumOctaves(10); +} + +void SPiano::setFont(CFontRef font) +{ + font_ = font; + getDimensions(true); + invalid(); +} + +void SPiano::setNumOctaves(unsigned octs) +{ + keyval_.resize(octs * 12); + octs_ = std::max(1u, octs); + getDimensions(true); + invalid(); +} + +void SPiano::draw(CDrawContext* dc) +{ + const Dimensions dim = getDimensions(false); + const unsigned octs = octs_; + const unsigned keyCount = octs * 12; + + dc->setDrawMode(kAntiAliasing); + + if (backgroundFill_.alpha > 0) { + SharedPointer path; + path = owned(dc->createGraphicsPath()); + path->addRoundRect(dim.bounds, backgroundRadius_); + dc->setFillColor(CColor(0xca, 0xca, 0xca)); + dc->drawGraphicsPath(path, CDrawContext::kPathFilled); + } + + for (unsigned key = 0; key < keyCount; ++key) { + if (!black[key % 12]) { + CRect rect = keyRect(key); + CColor keycolor = whiteFill_; + if (keyval_[key]) + keycolor = pressedFill_; + dc->setFillColor(keycolor); + dc->drawRect(rect, kDrawFilled); + } + } + + dc->setFrameColor(outline_); + dc->drawLine(dim.keyBounds.getTopLeft(), dim.keyBounds.getBottomLeft()); + for (unsigned key = 0; key < keyCount; ++key) { + if (!black[key % 12]) { + CRect rect = keyRect(key); + dc->drawLine(rect.getTopRight(), rect.getBottomRight()); + } + } + + for (unsigned key = 0; key < keyCount; ++key) { + if (black[key % 12]) { + CRect rect = keyRect(key); + CColor keycolor = blackFill_; + if (keyval_[key]) + keycolor = pressedFill_; + dc->setFillColor(keycolor); + dc->drawRect(rect, kDrawFilled); + dc->setFrameColor(outline_); + dc->drawRect(rect); + } + } + + if (const CFontRef& font = font_) { + for (unsigned o = 0; o < octs; ++o) { + CRect rect = keyRect(o * 12); + CRect textRect( + rect.left, dim.labelBounds.top, + rect.right, dim.labelBounds.bottom); + dc->setFont(font_); + dc->setFontColor(labelStroke_); + std::string text = std::to_string(static_cast(o) - 1); + dc->drawString(text.c_str(), textRect, kCenterText); + } + } + + { + dc->setFrameColor(outline_); + dc->drawLine(dim.keyBounds.getTopLeft(), dim.keyBounds.getTopRight()); + dc->setFrameColor(shadeOutline_); + dc->drawLine(dim.keyBounds.getBottomLeft(), dim.keyBounds.getBottomRight()); + } + + dc->setFrameColor(outline_); +} + +CMouseEventResult SPiano::onMouseDown(CPoint& where, const CButtonState& buttons) +{ + unsigned key = keyAtPos(where); + if (key != ~0u) { + keyval_[key] = 1; + mousePressedKey_ = key; + if (onKeyPressed) + onKeyPressed(key, mousePressVelocity(key, where.y)); + invalid(); + return kMouseEventHandled; + } + return CView::onMouseDown(where, buttons); +} + +CMouseEventResult SPiano::onMouseUp(CPoint& where, const CButtonState& buttons) +{ + unsigned key = mousePressedKey_; + if (key != ~0u) { + keyval_[key] = 0; + if (onKeyReleased) + onKeyReleased(key, mousePressVelocity(key, where.y)); + mousePressedKey_ = ~0u; + invalid(); + return kMouseEventHandled; + } + return CView::onMouseUp(where, buttons); +} + +CMouseEventResult SPiano::onMouseMoved(CPoint& where, const CButtonState& buttons) +{ + if (mousePressedKey_ != ~0u) { + unsigned key = keyAtPos(where); + if (mousePressedKey_ != key) { + keyval_[mousePressedKey_] = 0; + if (onKeyReleased) + onKeyReleased(mousePressedKey_, mousePressVelocity(key, where.y)); + // mousePressedKey_ = ~0u; + if (key != ~0u) { + keyval_[key] = 1; + mousePressedKey_ = key; + if (onKeyPressed) + onKeyPressed(key, mousePressVelocity(key, where.y)); + } + invalid(); + } + return kMouseEventHandled; + } + return CView::onMouseMoved(where, buttons); +} + +const SPiano::Dimensions& SPiano::getDimensions(bool forceUpdate) const +{ + if (!forceUpdate && dim_.bounds == getViewSize()) + return dim_; + + Dimensions dim; + dim.bounds = getViewSize(); + dim.paddedBounds = CRect(dim.bounds) + .extend(-innerPaddingX_, -innerPaddingY_); + CCoord keyHeight = std::floor(dim.paddedBounds.getHeight()); + CCoord fontHeight = font_ ? font_->getSize() : 0.0; + keyHeight -= spacingY_ + fontHeight; + dim.keyBounds = CRect(dim.paddedBounds) + .setHeight(keyHeight); + dim.keyWidth = static_cast( + dim.paddedBounds.getWidth() / octs_ / 7.0); + dim.keyBounds.setWidth(dim.keyWidth * octs_ * 7.0); + dim.keyBounds.offset( + 0.5 * (dim.paddedBounds.getWidth() - dim.keyBounds.getWidth()), 0.0); + + if (!font_) + dim.labelBounds = CRect(); + else + dim.labelBounds = CRect( + dim.keyBounds.left, dim.keyBounds.bottom, + dim.keyBounds.right, dim.keyBounds.bottom + (spacingY_ + fontHeight)); + + dim_ = dim; + return dim_; +} + +CRect SPiano::keyRect(const Dimensions& dim, unsigned key) +{ + unsigned oct = key / 12; + unsigned note = key % 12; + unsigned keyw = dim.keyWidth; + unsigned keyh = static_cast(dim.keyBounds.getHeight()); + CCoord octwidth = (keyoffs[11] + 1.0) * keyw; + CCoord octx = octwidth * oct; + CCoord notex = octx + keyoffs[note] * keyw; + CCoord notew = black[note] ? (0.6 * keyw) : keyw; + CCoord noteh = black[note] ? (0.6 * keyh) : keyh; + return CRect(notex, 0.0, notex + notew, noteh).offset(dim.keyBounds.getTopLeft()); +} + +CRect SPiano::keyRect(unsigned key) const +{ + return keyRect(getDimensions(false), key); +} + +unsigned SPiano::keyAtPos(CPoint pos) const +{ + const unsigned octs = octs_; + + for (unsigned key = 0; key < octs * 12; ++key) { + if (black[key % 12]) { + if (keyRect(key).pointInside(pos)) + return key; + } + } + + for (unsigned key = 0; key < octs * 12; ++key) { + if (!black[key % 12]) { + if (keyRect(key).pointInside(pos)) + return key; + } + } + + return ~0u; +} + +float SPiano::mousePressVelocity(unsigned key, CCoord posY) +{ + const CRect rect = keyRect(key); + CCoord value = (posY - rect.top) / rect.getHeight(); + return std::max(0.0f, std::min(1.0f, static_cast(value))); +} diff --git a/editor/src/editor/GUIPiano.h b/editor/src/editor/GUIPiano.h new file mode 100644 index 00000000..6927b06f --- /dev/null +++ b/editor/src/editor/GUIPiano.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "utility/vstgui_before.h" +#include "vstgui/lib/cview.h" +#include "vstgui/lib/ccolor.h" +#include "utility/vstgui_after.h" +#include +#include + +using namespace VSTGUI; + +class SPiano : public CView { +public: + explicit SPiano(CRect bounds); + + CFontRef getFont() const { return font_; } + void setFont(CFontRef font); + + unsigned getNumOctaves() const { return octs_; } + void setNumOctaves(unsigned octs); + + std::function onKeyPressed; + std::function onKeyReleased; + +protected: + void draw(CDrawContext* dc) override; + CMouseEventResult onMouseDown(CPoint& where, const CButtonState& buttons) override; + CMouseEventResult onMouseUp(CPoint& where, const CButtonState& buttons) override; + CMouseEventResult onMouseMoved(CPoint& where, const CButtonState& buttons) override; + +private: + struct Dimensions { + CRect bounds {}; + CRect paddedBounds {}; + CRect keyBounds {}; + unsigned keyWidth {}; + CRect labelBounds {}; + }; + const Dimensions& getDimensions(bool forceUpdate) const; + + static CRect keyRect(const Dimensions& dim, unsigned key); + CRect keyRect(unsigned key) const; + unsigned keyAtPos(CPoint pos) const; + + float mousePressVelocity(unsigned key, CCoord posY); + +private: + unsigned octs_ {}; + std::vector keyval_; + unsigned mousePressedKey_ = ~0u; + + CCoord innerPaddingX_ = 4.0; + CCoord innerPaddingY_ = 4.0; + CCoord spacingY_ = 4.0; + + CColor backgroundFill_ { 0xca, 0xca, 0xca, 0xff }; + float backgroundRadius_ = 5.0; + CColor whiteFill_ { 0xee, 0xee, 0xec, 0xff }; + CColor blackFill_ { 0x2e, 0x34, 0x36, 0xff }; + CColor pressedFill_ { 0xa0, 0xa0, 0xa0, 0xff }; + CColor outline_ { 0x00, 0x00, 0x00, 0xff }; + CColor shadeOutline_ { 0x80, 0x80, 0x80, 0xff }; + CColor labelStroke_ { 0x63, 0x63, 0x63, 0xff }; + + mutable Dimensions dim_; + SharedPointer font_; +}; diff --git a/editor/src/editor/layout/main.hpp b/editor/src/editor/layout/main.hpp index c93cf64e..0c9c61e2 100644 --- a/editor/src/editor/layout/main.hpp +++ b/editor/src/editor/layout/main.hpp @@ -69,7 +69,6 @@ enterTheme(defaultTheme); LogicalGroup* const view__28 = createLogicalGroup(CRect(5, 110, 796, 395), -1, "", kCenterText, 14); subPanels_[kPanelGeneral] = view__28; view__0->addView(view__28); -view__28->setVisible(false); RoundedGroup* const view__29 = createRoundedGroup(CRect(0, 0, 175, 280), -1, "", kCenterText, 14); view__28->addView(view__29); Label* const view__30 = createLabel(CRect(15, 10, 75, 35), -1, "Curves:", kLeftText, 14); @@ -108,6 +107,7 @@ view__41->addView(view__42); LogicalGroup* const view__43 = createLogicalGroup(CRect(5, 109, 795, 395), -1, "", kCenterText, 14); subPanels_[kPanelSettings] = view__43; view__0->addView(view__43); +view__43->setVisible(false); TitleGroup* const view__44 = createTitleGroup(CRect(255, 26, 535, 126), -1, "Engine", kCenterText, 12); view__43->addView(view__44); ValueMenu* const view__45 = createValueMenu(CRect(25, 60, 85, 85), kTagSetNumVoices, "", kCenterText, 12); @@ -150,5 +150,6 @@ view__51->addView(view__59); ValueMenu* const view__60 = createValueMenu(CRect(170, 60, 200, 85), kTagSetScalaRootKey, "", kCenterText, 12); scalaRootOctaveSlider_ = view__60; view__51->addView(view__60); -Piano* const view__61 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 14); +Piano* const view__61 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 12); +piano_ = view__61; view__0->addView(view__61); From 82f579bf0aed4597e42f86c348484f50d3212db5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 14 Oct 2020 19:01:40 +0200 Subject: [PATCH 433/445] Fix some spacing and alignment issues --- editor/src/editor/GUIPiano.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/editor/src/editor/GUIPiano.cpp b/editor/src/editor/GUIPiano.cpp index 6d4919bb..dd7d6cf5 100644 --- a/editor/src/editor/GUIPiano.cpp +++ b/editor/src/editor/GUIPiano.cpp @@ -10,6 +10,7 @@ #include "vstgui/lib/cgraphicspath.h" #include "utility/vstgui_after.h" #include +#include static constexpr CCoord keyoffs[12] = {0, 0.6, 1, 1.8, 2, 3, 3.55, 4, 4.7, 5, 5.85, 6}; @@ -166,7 +167,7 @@ const SPiano::Dimensions& SPiano::getDimensions(bool forceUpdate) const Dimensions dim; dim.bounds = getViewSize(); dim.paddedBounds = CRect(dim.bounds) - .extend(-innerPaddingX_, -innerPaddingY_); + .extend(-2 * innerPaddingX_, -2 * innerPaddingY_); CCoord keyHeight = std::floor(dim.paddedBounds.getHeight()); CCoord fontHeight = font_ ? font_->getSize() : 0.0; keyHeight -= spacingY_ + fontHeight; @@ -176,14 +177,14 @@ const SPiano::Dimensions& SPiano::getDimensions(bool forceUpdate) const dim.paddedBounds.getWidth() / octs_ / 7.0); dim.keyBounds.setWidth(dim.keyWidth * octs_ * 7.0); dim.keyBounds.offset( - 0.5 * (dim.paddedBounds.getWidth() - dim.keyBounds.getWidth()), 0.0); + std::floor(0.5 * (dim.paddedBounds.getWidth() - dim.keyBounds.getWidth())), 0.0); if (!font_) dim.labelBounds = CRect(); else dim.labelBounds = CRect( - dim.keyBounds.left, dim.keyBounds.bottom, - dim.keyBounds.right, dim.keyBounds.bottom + (spacingY_ + fontHeight)); + dim.keyBounds.left, dim.keyBounds.bottom + spacingY_, + dim.keyBounds.right, dim.keyBounds.bottom + spacingY_ + fontHeight); dim_ = dim; return dim_; From 1e6cdd841b60fc32f27e5300d835bbc3d6ff0fa8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 14 Oct 2020 20:04:59 +0200 Subject: [PATCH 434/445] Implement MIDI sending to VST from UI --- vst/SfizzVstEditor.cpp | 16 +++++++++--- vst/SfizzVstEditor.h | 2 +- vst/SfizzVstProcessor.cpp | 51 ++++++++++++++++++++++++++++++++++++++- vst/SfizzVstProcessor.h | 2 ++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/vst/SfizzVstEditor.cpp b/vst/SfizzVstEditor.cpp index 1d0a127f..9746cdf9 100644 --- a/vst/SfizzVstEditor.cpp +++ b/vst/SfizzVstEditor.cpp @@ -176,10 +176,20 @@ void SfizzVstEditor::uiEndSend(EditId id) getController()->endEdit(pid); } -void SfizzVstEditor::uiSendMIDI(const uint8_t* msg, uint32_t len) +void SfizzVstEditor::uiSendMIDI(const uint8_t* data, uint32_t len) { - // TODO send MIDI... - + SfizzVstController* ctl = getController(); + + Steinberg::OPtr msg { ctl->allocateMessage() }; + if (!msg) { + fprintf(stderr, "[Sfizz] UI could not allocate message\n"); + return; + } + + msg->setMessageID("MidiMessage"); + Vst::IAttributeList* attr = msg->getAttributes(); + attr->setBinary("Data", data, len); + ctl->sendMessage(msg); } /// diff --git a/vst/SfizzVstEditor.h b/vst/SfizzVstEditor.h index 9c918765..a0730220 100644 --- a/vst/SfizzVstEditor.h +++ b/vst/SfizzVstEditor.h @@ -40,7 +40,7 @@ protected: void uiSendValue(EditId id, const EditValue& v) override; void uiBeginSend(EditId id) override; void uiEndSend(EditId id) override; - void uiSendMIDI(const uint8_t* msg, uint32_t len) override; + void uiSendMIDI(const uint8_t* data, uint32_t len) override; private: void loadSfzFile(const std::string& filePath); diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index 441fd9df..6f729a27 100644 --- a/vst/SfizzVstProcessor.cpp +++ b/vst/SfizzVstProcessor.cpp @@ -22,8 +22,10 @@ static const char defaultSfzText[] = "sample=*sine" "\n" "ampeg_attack=0.02 ampeg_release=0.1" "\n"; +enum { kMidiEventMaximumSize = 4 }; + SfizzVstProcessor::SfizzVstProcessor() - : _fifoToWorker(64 * 1024) + : _fifoToWorker(64 * 1024), _fifoMidiFromUi(64 * 1024) { setControllerClass(SfizzVstController::cid); } @@ -181,6 +183,8 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) else synth.disableFreeWheeling(); + processMidiFromUi(); + if (Vst::IParameterChanges* pc = data.inputParameterChanges) processControllerChanges(*pc); @@ -373,6 +377,40 @@ void SfizzVstProcessor::processEvents(Vst::IEventList& events) } } +void SfizzVstProcessor::processMidiFromUi() +{ + sfz::Sfizz& synth = *_synth; + + for (uint32 size = 0; _fifoMidiFromUi.peek(size) && + _fifoMidiFromUi.size_used() >= sizeof(size) + size; ) { + _fifoMidiFromUi.discard(sizeof(size)); + + if (size > kMidiEventMaximumSize) { + _fifoMidiFromUi.discard(size); + continue; + } + + uint8_t data[kMidiEventMaximumSize] = {}; + _fifoMidiFromUi.get(data, size); + + // interpret the MIDI message + switch (data[0] & 0xf0) { + case 0x80: + synth.noteOff(0, data[1] & 0x7f, data[2] & 0x7f); + break; + case 0x90: + synth.noteOn(0, data[1] & 0x7f, data[2] & 0x7f); + break; + case 0xb0: + synth.cc(0, data[1] & 0x7f, data[2] & 0x7f); + break; + case 0xe0: + synth.pitchWheel(0, (data[2] << 7) + data[1] - 8192); + break; + } + } +} + int SfizzVstProcessor::convertVelocityFromFloat(float x) { return std::min(127, std::max(0, (int)(x * 127.0f))); @@ -425,6 +463,17 @@ tresult PLUGIN_API SfizzVstProcessor::notify(Vst::IMessage* message) reply->getAttributes()->setBinary("File", _state.scalaFile.data(), _state.scalaFile.size()); sendMessage(reply); } + else if (!std::strcmp(id, "MidiMessage")) { + const void* data = nullptr; + uint32 size = 0; + result = attr->getBinary("Data", data, size); + if (size < kMidiEventMaximumSize) { + if (_fifoMidiFromUi.size_free() >= sizeof(size) + size) { + _fifoMidiFromUi.put(size); + _fifoMidiFromUi.put(reinterpret_cast(data), size); + } + } + } return result; } diff --git a/vst/SfizzVstProcessor.h b/vst/SfizzVstProcessor.h index 1eb68cea..7f388621 100644 --- a/vst/SfizzVstProcessor.h +++ b/vst/SfizzVstProcessor.h @@ -35,6 +35,7 @@ public: void processParameterChanges(Vst::IParameterChanges& pc); void processControllerChanges(Vst::IParameterChanges& pc); void processEvents(Vst::IEventList& events); + void processMidiFromUi(); static int convertVelocityFromFloat(float x); tresult PLUGIN_API notify(Vst::IMessage* message) override; @@ -58,6 +59,7 @@ private: volatile bool _workRunning = false; Ring_Buffer _fifoToWorker; RTSemaphore _semaToWorker; + Ring_Buffer _fifoMidiFromUi; std::mutex _processMutex; // file modification periodic checker From 07f30af7f173097c6b8ef0c28bbe37384d6f13e8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 14 Oct 2020 23:21:10 +0200 Subject: [PATCH 435/445] Replace stable_sort by sort --- src/sfizz/VoiceStealing.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 1c425ac7..878f5fb8 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -33,13 +33,13 @@ sfz::Voice* sfz::VoiceStealing::stealFirst(absl::Span voices) noexcept sfz::Voice* sfz::VoiceStealing::stealOldest(absl::Span voices) noexcept { - absl::c_stable_sort(voices, voiceOrdering); + absl::c_sort(voices, voiceOrdering); return voices.front(); } sfz::Voice* sfz::VoiceStealing::stealEnvelopeAndAge(absl::Span voices) noexcept { - absl::c_stable_sort(voices, voiceOrdering); + absl::c_sort(voices, voiceOrdering); const auto sumPower = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { return sum + v->getAveragePower(); From f3776dcb3579c631fa5f31cdc5460ce372bf470a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 14 Oct 2020 23:31:55 +0200 Subject: [PATCH 436/445] Set the power follower depending on the voice stealing algorithm also on voice re-creation --- src/sfizz/Synth.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 3a872727..0f11bba4 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1568,6 +1568,15 @@ void sfz::Synth::applySettingsPerVoice() voice->setPitchEGEnabledPerVoice(settingsPerVoice.havePitchEG); voice->setFilterEGEnabledPerVoice(settingsPerVoice.haveFilterEG); } + + if (stealer.getStealingAlgorithm() == + VoiceStealing::StealingAlgorithm::EnvelopeAndAge) { + for (auto& voice : voices) + voice->enablePowerFollower(); + } else { + for (auto& voice : voices) + voice->disablePowerFollower(); + } } void sfz::Synth::setupModMatrix() From e258ad254f78c425450032e6fe887c7c7b17a899 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 15 Oct 2020 13:14:17 +0200 Subject: [PATCH 437/445] Make all phase opcodes normalized --- src/sfizz/Defaults.h | 6 +++--- src/sfizz/MathHelpers.h | 11 +++++++++++ src/sfizz/Region.cpp | 18 +++++++----------- src/sfizz/effects/Apan.cpp | 7 ++----- tests/RegionT.cpp | 12 ++++++------ tests/lfo/lfo_subwave.sfz | 8 ++++---- tests/lfo/lfo_waves.sfz | 2 +- 7 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 24fc06c3..290c1248 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -65,7 +65,7 @@ namespace Default // Wavetable oscillator constexpr float oscillatorPhase { 0.0 }; - constexpr Range oscillatorPhaseRange { -1.0, 360.0 }; + constexpr Range oscillatorPhaseRange { -1.0, 1.0 }; constexpr int oscillatorMode { 0 }; constexpr int oscillatorMulti { 1 }; constexpr Range oscillatorModeRange { 0, 2 }; @@ -222,7 +222,7 @@ namespace Default constexpr int numLFOSubs { 2 }; constexpr int numLFOSteps { 8 }; constexpr Range lfoFreqRange { 0.0, 100.0 }; - constexpr Range lfoPhaseRange { 0.0, 360.0 }; + constexpr Range lfoPhaseRange { 0.0, 1.0 }; constexpr Range lfoDelayRange { 0.0, 30.0 }; constexpr Range lfoFadeRange { 0.0, 30.0 }; constexpr Range lfoCountRange { 0, 1000 }; @@ -281,7 +281,7 @@ namespace Default constexpr Range apanWaveformRange { 0, std::numeric_limits::max() }; constexpr Range apanFrequencyRange { 0, std::numeric_limits::max() }; - constexpr Range apanPhaseRange { 0.0, 360.0 }; + constexpr Range apanPhaseRange { 0.0, 1.0 }; constexpr Range apanLevelRange { 0.0, 100.0 }; } } diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index 9fb035f1..50b012bd 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -288,6 +288,17 @@ constexpr long int lroundPositive(T value) return static_cast(0.5f + value); // NOLINT } +/** + @brief Wrap a normalized phase into the domain [0;1[ + */ +template +static T wrapPhase(T phase) +{ + T wrapped = phase - static_cast(phase); + wrapped += wrapped < 0; + return wrapped; +} + /** @brief A fraction which is parameterized by integer type */ diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index be7959ef..810b0d79 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -144,7 +144,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) // Wavetable oscillator case hash("oscillator_phase"): - setValueFromOpcode(opcode, oscillatorPhase, Default::oscillatorPhaseRange); + if (auto value = readOpcode(opcode.value, Default::oscillatorPhaseRange)) + oscillatorPhase = (*value >= 0) ? wrapPhase(*value) : -1.0f; break; case hash("oscillator"): if (auto value = readBooleanFromOpcode(opcode)) @@ -851,12 +852,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; if (!extendIfNecessary(lfos, lfoNumber, Default::numLFOs)) return false; - if (auto value = readOpcode(opcode.value, Default::lfoPhaseRange)) { - float normalPhase = *value * (1.0 / 360.0); - normalPhase -= int(normalPhase); - normalPhase += (normalPhase < 0) ? 1 : 0; - lfos[lfoNumber - 1].phase0 = normalPhase; - } + if (auto value = readOpcode(opcode.value, Default::lfoPhaseRange)) + lfos[lfoNumber - 1].phase0 = wrapPhase(*value); } break; case hash("lfo&_delay"): @@ -1762,10 +1759,9 @@ float sfz::Region::getBaseGain() const noexcept float sfz::Region::getPhase() const noexcept { float phase; - if (oscillatorPhase >= 0) { - phase = oscillatorPhase * (1.0f / 360.0f); - phase -= static_cast(static_cast(phase)); - } else { + if (oscillatorPhase >= 0) + phase = oscillatorPhase; + else { fast_real_distribution phaseDist { 0.0001f, 0.9999f }; phase = phaseDist(Random::randomGenerator); } diff --git a/src/sfizz/effects/Apan.cpp b/src/sfizz/effects/Apan.cpp index c21af6c3..a51f4a4e 100644 --- a/src/sfizz/effects/Apan.cpp +++ b/src/sfizz/effects/Apan.cpp @@ -89,11 +89,8 @@ namespace fx { apan->_lfoFrequency = *value; break; case hash("apan_phase"): - if (auto value = readOpcode(opc.value, Default::apanPhaseRange)) { - float phase = *value / 360.0f; - phase -= static_cast(phase); - apan->_lfoPhaseOffset = phase; - } + if (auto value = readOpcode(opc.value, Default::apanPhaseRange)) + apan->_lfoPhaseOffset = wrapPhase(*value); break; case hash("apan_dry"): if (auto value = readOpcode(opc.value, Default::apanLevelRange)) diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 8bd77f62..945ae0bf 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1595,14 +1595,14 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("Wavetable phase") { REQUIRE(region.oscillatorPhase == 0.0f); - region.parseOpcode({ "oscillator_phase", "45" }); - REQUIRE(region.oscillatorPhase == 45.0f); - region.parseOpcode({ "oscillator_phase", "45.32" }); - REQUIRE(region.oscillatorPhase == 45.32_a); + region.parseOpcode({ "oscillator_phase", "0.25" }); + REQUIRE(region.oscillatorPhase == 0.25f); + region.parseOpcode({ "oscillator_phase", "0.3" }); + REQUIRE(region.oscillatorPhase == 0.3_a); region.parseOpcode({ "oscillator_phase", "-1" }); REQUIRE(region.oscillatorPhase == -1.0f); - region.parseOpcode({ "oscillator_phase", "361" }); - REQUIRE(region.oscillatorPhase == 360.0f); + region.parseOpcode({ "oscillator_phase", "1.1" }); + REQUIRE(region.oscillatorPhase == 0.0f); } SECTION("Note polyphony") diff --git a/tests/lfo/lfo_subwave.sfz b/tests/lfo/lfo_subwave.sfz index eb493764..6aa09f79 100644 --- a/tests/lfo/lfo_subwave.sfz +++ b/tests/lfo/lfo_subwave.sfz @@ -7,23 +7,23 @@ fil_type=brf_2p lfo1_cutoff=1200.0 // lfo1_freq=1 -lfo1_phase=180 +lfo1_phase=0.5 lfo1_wave=3 // lfo2_freq=1 -lfo2_phase=180 +lfo2_phase=0.5 lfo2_wave=3 lfo2_wave2=1 // lfo3_freq=1 -lfo3_phase=180 +lfo3_phase=0.5 lfo3_wave=3 lfo3_wave2=1 lfo3_ratio2=2 // lfo4_freq=1 -lfo4_phase=180 +lfo4_phase=0.5 lfo4_wave=3 lfo4_wave2=1 lfo4_ratio2=2 diff --git a/tests/lfo/lfo_waves.sfz b/tests/lfo/lfo_waves.sfz index 091245dd..07db6b3c 100644 --- a/tests/lfo/lfo_waves.sfz +++ b/tests/lfo/lfo_waves.sfz @@ -18,6 +18,6 @@ lfo6_wave=5 lfo6_freq=2.0 lfo7_wave=6 lfo7_freq=1.0 -lfo7_phase=180 +lfo7_phase=0.5 lfo8_wave=7 lfo8_freq=2.0 From a5647ab33943302c4f99bd9cd61e5745510bd362 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 15 Oct 2020 14:30:15 +0100 Subject: [PATCH 438/445] Add a mutex on the loading jobs to protect them in the case of freewheeling --- src/sfizz/FilePool.cpp | 4 ++++ src/sfizz/FilePool.h | 1 + 2 files changed, 5 insertions(+) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 6c3a08df..ca437d52 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -482,6 +482,7 @@ void sfz::FilePool::dispatchingJob() noexcept } // Clear finished jobs + std::lock_guard guard { loadingJobsMutex }; swapAndPopAll(loadingJobs, [](std::future& future) { return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }); @@ -515,8 +516,11 @@ void sfz::FilePool::emptyFileLoadingQueues() noexcept void sfz::FilePool::waitForBackgroundLoading() noexcept { + std::lock_guard guard { loadingJobsMutex }; + for (auto& job : loadingJobs) job.wait(); + loadingJobs.clear(); } diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 109394b4..94f2fb2d 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -354,6 +354,7 @@ private: void dispatchingJob() noexcept; void garbageJob() noexcept; void loadingJob(QueuedFileData data) noexcept; + SpinMutex loadingJobsMutex; std::vector> loadingJobs; std::thread dispatchThread { &FilePool::dispatchingJob, this }; std::thread garbageThread { &FilePool::garbageJob, this }; From 67af16f0e2c7a349217edbb36145feb6617c25ee Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 15 Oct 2020 15:58:08 +0200 Subject: [PATCH 439/445] Move the lock guard --- src/sfizz/FilePool.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index ca437d52..5be67c6e 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -476,13 +476,14 @@ void sfz::FilePool::dispatchingJob() noexcept continue; } + std::lock_guard guard { loadingJobsMutex }; + if (filesToLoad.try_pop(queuedData)) { loadingJobs.push_back( threadPool.enqueue([this](const QueuedFileData& data) { loadingJob(data); }, queuedData)); } // Clear finished jobs - std::lock_guard guard { loadingJobsMutex }; swapAndPopAll(loadingJobs, [](std::future& future) { return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }); From f4b6323050da7620972e01b32ebc102e9b7a0aba Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 15 Oct 2020 16:44:08 +0100 Subject: [PATCH 440/445] Use std::mutex --- src/sfizz/FilePool.cpp | 4 ++-- src/sfizz/FilePool.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 5be67c6e..cb7faa47 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -476,7 +476,7 @@ void sfz::FilePool::dispatchingJob() noexcept continue; } - std::lock_guard guard { loadingJobsMutex }; + std::lock_guard guard { loadingJobsMutex }; if (filesToLoad.try_pop(queuedData)) { loadingJobs.push_back( @@ -517,7 +517,7 @@ void sfz::FilePool::emptyFileLoadingQueues() noexcept void sfz::FilePool::waitForBackgroundLoading() noexcept { - std::lock_guard guard { loadingJobsMutex }; + std::lock_guard guard { loadingJobsMutex }; for (auto& job : loadingJobs) job.wait(); diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 94f2fb2d..a44ce948 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -354,7 +354,7 @@ private: void dispatchingJob() noexcept; void garbageJob() noexcept; void loadingJob(QueuedFileData data) noexcept; - SpinMutex loadingJobsMutex; + std::mutex loadingJobsMutex; std::vector> loadingJobs; std::thread dispatchThread { &FilePool::dispatchingJob, this }; std::thread garbageThread { &FilePool::garbageJob, this }; From bd4057e9272f888456d9b4ecb1bbf7433f891529 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 15 Oct 2020 16:51:25 +0100 Subject: [PATCH 441/445] Use the future helper --- src/sfizz/FilePool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index cb7faa47..056c76d6 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -485,7 +485,7 @@ void sfz::FilePool::dispatchingJob() noexcept // Clear finished jobs swapAndPopAll(loadingJobs, [](std::future& future) { - return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; + return is_ready(future); }); } } From 72dfd549334e1cc3f64560bdd718748159a87e6c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 15 Oct 2020 19:38:07 +0200 Subject: [PATCH 442/445] Remove the warning suppression which broke OBS The error was: -Wformat-security ignored without -Wformat --- vst/CMakeLists.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index ecbbab4a..cddf56b2 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -108,8 +108,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") "-Wno-unknown-pragmas" "-Wno-unused-function" "-Wno-unused-parameter" - "-Wno-unused-variable" - "-Wno-format") + "-Wno-unused-variable") endif() # To help debugging the link only @@ -293,8 +292,7 @@ elseif(SFIZZ_AU) "-Wno-unknown-pragmas" "-Wno-unused-function" "-Wno-unused-parameter" - "-Wno-unused-variable" - "-Wno-format") + "-Wno-unused-variable") endif() # Installation From 80f0f4d90af5210d24ee6f1af33acd529ae29b05 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 15 Oct 2020 19:53:10 +0200 Subject: [PATCH 443/445] Fix a build error using older sndfile --- src/sfizz/AudioReader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/AudioReader.cpp b/src/sfizz/AudioReader.cpp index aacef1bf..e906e3ae 100644 --- a/src/sfizz/AudioReader.cpp +++ b/src/sfizz/AudioReader.cpp @@ -139,7 +139,7 @@ private: ReverseReader::ReverseReader(SndfileHandle handle) : BasicSndfileReader(handle) { - position_ = handle.seek(0, SF_SEEK_END); + position_ = handle.seek(0, SEEK_END); } AudioReaderType ReverseReader::type() const From 0cd7cb9e3b9a5659118e287eb19e3985e9dca851 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 15 Oct 2020 21:44:49 +0200 Subject: [PATCH 444/445] Update version to 0.5.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 19aa16d9..dcd2f9b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ else() endif() endif() -project (sfizz VERSION 0.4.1 LANGUAGES CXX C) +project (sfizz VERSION 0.5.0 LANGUAGES CXX C) set (PROJECT_DESCRIPTION "A library to load SFZ description files and use them to render music.") # External configuration CMake scripts From a0e893964fb58ef90d709f21c77620f66a0800e7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 15 Oct 2020 21:48:09 +0200 Subject: [PATCH 445/445] Update `since` to reflect upcoming the stable version --- src/sfizz.h | 8 ++++---- src/sfizz.hpp | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sfizz.h b/src/sfizz.h index 5b2ea170..ec47ad04 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -46,7 +46,7 @@ typedef enum { /** * @brief Processing mode - * @since 0.4.1 + * @since 0.5.0 */ typedef enum { SFIZZ_PROCESS_LIVE, @@ -356,7 +356,7 @@ SFIZZ_EXPORTED_API void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float /** * @brief Send the time signature. - * @since 0.4.1 + * @since 0.5.0 * * @param synth The synth. * @param delay The delay. @@ -367,7 +367,7 @@ SFIZZ_EXPORTED_API void sfizz_send_time_signature(sfizz_synth_t* synth, int dela /** * @brief Send the time position. - * @since 0.4.1 + * @since 0.5.0 * * @param synth The synth. * @param delay The delay. @@ -378,7 +378,7 @@ SFIZZ_EXPORTED_API void sfizz_send_time_position(sfizz_synth_t* synth, int delay /** * @brief Send the playback state. - * @since 0.4.1 + * @since 0.5.0 * * @param synth The synth. * @param delay The delay. diff --git a/src/sfizz.hpp b/src/sfizz.hpp index ab39323a..81ba6cae 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -326,7 +326,7 @@ public: /** * @brief Send the time signature. - * @since 0.4.1 + * @since 0.5.0 * * @param delay The delay. * @param beatsPerBar The number of beats per bar, or time signature numerator. @@ -336,7 +336,7 @@ public: /** * @brief Send the time position. - * @since 0.4.1 + * @since 0.5.0 * * @param delay The delay. * @param bar The current bar. @@ -346,7 +346,7 @@ public: /** * @brief Send the playback state. - * @since 0.4.1 + * @since 0.5.0 * * @param delay The delay. * @param playbackState The playback state, 1 if playing, 0 if stopped.

& pairVector, const T& key, U value, bool replace = true) +{ + bool result = false; + auto it = absl::c_find_if( + pairVector, [&key](const P& pair) { return pair.first == key; }); + if (it != pairVector.end()) { + if (replace) { + it->second = std::move(value); + result = true; + } + } + else { + pairVector.emplace_back(key, std::move(value)); + result = true; + } + return result; +} + /** * @brief From a source view, find the next sfz header and its members and * return them, while updating the source by removing this header diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 32ceccad..bd4f177c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -233,8 +233,8 @@ void sfz::Synth::clear() hdcc(0, 10, 0.5f); // pan // set default controller labels - ccLabels.emplace_back(7, "Volume"); - ccLabels.emplace_back(10, "Pan"); + insertPairUniquely(ccLabels, 7, "Volume"); + insertPairUniquely(ccLabels, 10, "Pan"); } void sfz::Synth::handleMasterOpcodes(const std::vector& members) @@ -336,12 +336,12 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) break; case hash("label_cc&"): if (Default::ccNumberRange.containsWithEnd(member.parameters.back())) - ccLabels.emplace_back(member.parameters.back(), std::string(member.value)); + insertPairUniquely(ccLabels, member.parameters.back(), std::string(member.value)); break; case hash("label_key&"): if (member.parameters.back() <= Default::keyRange.getEnd()) { const auto noteNumber = static_cast(member.parameters.back()); - keyLabels.emplace_back(noteNumber, std::string(member.value)); + insertPairUniquely(keyLabels, noteNumber, std::string(member.value)); } break; case hash("default_path"): @@ -545,7 +545,7 @@ void sfz::Synth::finalizeSfzLoad() } if (region->keyswitchLabel && region->keyswitch) - keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel }); + insertPairUniquely(keyswitchLabels, *region->keyswitch, *region->keyswitchLabel); // Some regions had group number but no "group-level" opcodes handled the polyphony while (polyphonyGroups.size() <= region->group) { From 3ca11652dced7d24b5305f04ca89e1a497c3bf1d Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 11:29:40 +0200 Subject: [PATCH 255/445] Add tests --- tests/FilesT.cpp | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index f257c410..e5a7c77f 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -591,11 +591,9 @@ TEST_CASE("[Files] Labels") REQUIRE( keyLabels[0].second == "Cymbals" ); REQUIRE( keyLabels[1].first == 65 ); REQUIRE( keyLabels[1].second == "Crash" ); - REQUIRE( ccLabels.size() == 2); - REQUIRE( ccLabels[0].first == 54 ); - REQUIRE( ccLabels[0].second == "Gain" ); - REQUIRE( ccLabels[1].first == 2 ); - REQUIRE( ccLabels[1].second == "Other" ); + REQUIRE( ccLabels.size() >= 2); + REQUIRE( absl::c_find(ccLabels, CCNamePair { 54, "Gain" }) != ccLabels.end() ); + REQUIRE( absl::c_find(ccLabels, CCNamePair { 2, "Other" }) != ccLabels.end() ); const std::string xmlMidnam = synth.exportMidnam(); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); @@ -612,3 +610,24 @@ TEST_CASE("[Files] Switch labels") REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); } + +TEST_CASE("[Files] Duplicate labels") +{ + sfz::Synth synth; + synth.loadSfzString( + fs::current_path() / "tests/TestFiles/labels.sfz", + R"( label_key60=Baz label_key60=Quux + label_cc20=Foo label_cc20=Bar + sample=*sine)"); + + auto keyLabels = synth.getKeyLabels(); + auto ccLabels = synth.getCCLabels(); + REQUIRE( keyLabels.size() == 1); + REQUIRE( keyLabels[0].first == 60 ); + REQUIRE( keyLabels[0].second == "Quux" ); + REQUIRE( ccLabels.size() >= 1); + REQUIRE( absl::c_find(ccLabels, CCNamePair { 20, "Bar" }) != ccLabels.end() ); + const std::string xmlMidnam = synth.exportMidnam(); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); +} From 169ba940c98f2e97f8373c5476b1d2f6652a69e1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 11:55:25 +0200 Subject: [PATCH 256/445] Print the modulation key more compactly --- src/sfizz/modulations/ModKey.cpp | 16 ++++++++-------- tests/FlexEGT.cpp | 14 +++++++------- tests/ModulationsT.cpp | 8 ++++---- tests/TestHelpers.cpp | 4 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 9012fb46..aa4dd1cd 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -71,22 +71,22 @@ std::string ModKey::toString() const " {curve=", params_.curve, ", smooth=", params_.smooth, ", value=", params_.value, ", step=", params_.step, "}"); case ModId::Envelope: - return absl::StrCat("EG ", 1 + params_.N, " {region=", region_.number(), "}"); + return absl::StrCat("EG ", 1 + params_.N, " {", region_.number(), "}"); case ModId::LFO: - return absl::StrCat("LFO ", 1 + params_.N, " {region=", region_.number(), "}"); + return absl::StrCat("LFO ", 1 + params_.N, " {", region_.number(), "}"); case ModId::Amplitude: - return absl::StrCat("Amplitude", " {region=", region_.number(), "}"); + return absl::StrCat("Amplitude {", region_.number(), "}"); case ModId::Pan: - return absl::StrCat("Pan", " {region=", region_.number(), "}"); + return absl::StrCat("Pan {", region_.number(), "}"); case ModId::Width: - return absl::StrCat("Width", " {region=", region_.number(), "}"); + return absl::StrCat("Width {", region_.number(), "}"); case ModId::Position: - return absl::StrCat("Position", " {region=", region_.number(), "}"); + return absl::StrCat("Position {", region_.number(), "}"); case ModId::Pitch: - return absl::StrCat("Pitch", " {region=", region_.number(), "}"); + return absl::StrCat("Pitch {", region_.number(), "}"); case ModId::Volume: - return absl::StrCat("Volume", " {region=", region_.number(), "}"); + return absl::StrCat("Volume {", region_.number(), "}"); default: return {}; diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp index f48e5455..db16f93c 100644 --- a/tests/FlexEGT.cpp +++ b/tests/FlexEGT.cpp @@ -41,7 +41,7 @@ TEST_CASE("[FlexEG] Values") REQUIRE( egDescription.points[4].level == 1.0_a ); REQUIRE( egDescription.sustain == 3 ); REQUIRE(synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ - R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + R"("EG 1 {0}" -> "Amplitude {0}")", })); } @@ -85,12 +85,12 @@ TEST_CASE("[FlexEG] Connections") REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); REQUIRE( synth.getRegionView(0)->flexEGs[0].points.size() == 2 ); REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ - R"("EG 1 {region=0}" -> "Amplitude {region=0}")", - R"("EG 1 {region=1}" -> "Pan {region=1}")", - R"("EG 1 {region=2}" -> "Width {region=2}")", - R"("EG 1 {region=3}" -> "Position {region=3}")", - R"("EG 1 {region=4}" -> "Pitch {region=4}")", - R"("EG 1 {region=5}" -> "Volume {region=5}")", + R"("EG 1 {0}" -> "Amplitude {0}")", + R"("EG 1 {1}" -> "Pan {1}")", + R"("EG 1 {2}" -> "Width {2}")", + R"("EG 1 {3}" -> "Position {3}")", + R"("EG 1 {4}" -> "Pitch {4}")", + R"("EG 1 {5}" -> "Volume {5}")", }, 6)); } diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 627714d6..2b14e191 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -93,9 +93,9 @@ width_oncc425=29 const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {region=0}")", - R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {region=0}")", - R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {region=0}")", - R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {region=0}")", + R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {0}")", + R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {0}")", + R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {0}")", + R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {0}")", })); } diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index fb4bd233..b3521c4a 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -73,12 +73,12 @@ std::string createReferenceGraph(std::vector lines, int numRegions) { for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { lines.push_back(absl::StrCat( - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {region=)", + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {)", regionIdx, R"(}")" )); lines.push_back(absl::StrCat( - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {region=)", + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {)", regionIdx, R"(}")" )); From 3cf7536c575b8e7c24451bb6e8ac9916e680d45d Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 14:45:32 +0200 Subject: [PATCH 257/445] Store the initial values of controllers --- src/sfizz/Synth.cpp | 30 ++++++++++++++++++++++++++---- src/sfizz/Synth.h | 24 ++++++++++++++++++++++++ tests/SynthT.cpp | 21 +++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c984acae..0065afd9 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -231,8 +231,9 @@ void sfz::Synth::clear() modificationTime = fs::file_time_type::min(); // set default controllers - cc(0, 7, 100); // volume - hdcc(0, 10, 0.5f); // pan + fill(absl::MakeSpan(ccInitialValues), 0.0f); + initCc(7, 100); // volume + initHdcc(10, 0.5f); // pan // set default controller labels insertPairUniquely(ccLabels, 7, "Volume"); @@ -326,14 +327,14 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) if (Default::ccNumberRange.containsWithEnd(member.parameters.back())) { const auto ccValue = readOpcode(member.value, Default::midi7Range); if (ccValue) - resources.midiState.ccEvent(0, member.parameters.back(), normalizeCC(*ccValue)); + initCc(member.parameters.back(), *ccValue); } break; case hash("set_hdcc&"): if (Default::ccNumberRange.containsWithEnd(member.parameters.back())) { const auto ccValue = readOpcode(member.value, Default::normalizedRange); if (ccValue) - resources.midiState.ccEvent(0, member.parameters.back(), *ccValue); + initHdcc(member.parameters.back(), *ccValue); } break; case hash("label_cc&"): @@ -1137,6 +1138,27 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept ccDispatch(delay, ccNumber, normValue); } +void sfz::Synth::initCc(int ccNumber, uint8_t ccValue) noexcept +{ + const float normValue = normalizeCC(ccValue); + initHdcc(ccNumber, normValue); +} + +void sfz::Synth::initHdcc(int ccNumber, float normValue) noexcept +{ + ASSERT(ccNumber >= 0); + ASSERT(ccNumber < config::numCCs); + ccInitialValues[ccNumber] = normValue; + resources.midiState.ccEvent(0, ccNumber, normValue); +} + +float sfz::Synth::getHdccInit(int ccNumber) +{ + ASSERT(ccNumber >= 0); + ASSERT(ccNumber < config::numCCs); + return ccInitialValues[ccNumber]; +} + void sfz::Synth::pitchWheel(int delay, int pitch) noexcept { ASSERT(pitch <= 8192); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index fd019dc3..278fe312 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -370,6 +370,27 @@ public: */ void hdcc(int delay, int ccNumber, float normValue) noexcept; /** + * @brief Set the initial value of a controller and send it to the synth + * + * @param ccNumber the cc number + * @param ccValue the cc value + */ + void initCc(int ccNumber, uint8_t ccValue) noexcept; + /** + * @brief Set the initial value of a controller and send it to the synth + * + * @param ccNumber the cc number + * @param normValue the normalized cc value, in domain 0 to 1 + */ + void initHdcc(int ccNumber, float normValue) noexcept; + /** + * @brief Get the initial value of a controller under the current instrument + * + * @param ccNumber the cc number + * @return the initial value + */ + float getHdccInit(int ccNumber); + /** * @brief Send a pitch bend event to the synth * * @param delay the delay at which the event occurs; this should be lower @@ -909,6 +930,9 @@ private: }; SettingsPerVoice settingsPerVoice; + // Controller initial values + std::array ccInitialValues; + Duration dispatchDuration { 0 }; Parser parser; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 2144426a..49fccde2 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1348,3 +1348,24 @@ TEST_CASE("[Synth] Off by with CC switches") REQUIRE( numPlayingVoices(synth) == 1 ); REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId.filename() == "*saw" ); } + +TEST_CASE("[Synth] Initial values of CC") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path() / "init_cc.sfz", R"( + sample=*sine + )"); + + REQUIRE(synth.getHdccInit(111) == 0.0f); + REQUIRE(synth.getHdccInit(7) == Approx(100.0f / 127)); // default volume + REQUIRE(synth.getHdccInit(10) == 0.5f); // default pan + + synth.loadSfzString(fs::current_path() / "init_cc.sfz", R"( + set_hdcc111=0.1234 set_cc112=77 + sample=*sine + )"); + + REQUIRE(synth.getHdccInit(111) == Approx(0.1234f)); + REQUIRE(synth.getHdccInit(112) == Approx(77.0f / 127)); +} From 8c625d1e04bb3b13c81fbb949659e41b7aee8100 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 12:00:23 +0200 Subject: [PATCH 258/445] Change filter cutoff modulation type to float --- src/sfizz/Defaults.h | 4 ++-- src/sfizz/FilterDescription.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index b12d1b8a..e6cc47f5 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -156,11 +156,11 @@ namespace Default constexpr uint8_t filterKeycenter { 60 }; constexpr int filterRandom { 0 }; constexpr int filterVeltrack { 0 }; - constexpr int filterCutoffCC { 0 }; + constexpr float filterCutoffCC { 0 }; constexpr float filterResonanceCC { 0 }; constexpr float filterGainCC { 0 }; constexpr Range filterCutoffRange { 0.0f, 20000.0f }; - constexpr Range filterCutoffModRange { -9600, 9600 }; + constexpr Range filterCutoffModRange { -9600, 9600 }; constexpr Range filterGainRange { -96.0f, 96.0f }; constexpr Range filterGainModRange { -96.0f, 96.0f }; constexpr Range filterKeytrackRange { 0, 1200 }; diff --git a/src/sfizz/FilterDescription.h b/src/sfizz/FilterDescription.h index b31e81e9..e21a9b58 100644 --- a/src/sfizz/FilterDescription.h +++ b/src/sfizz/FilterDescription.h @@ -22,7 +22,7 @@ struct FilterDescription int veltrack { Default::filterVeltrack }; int random { Default::filterRandom }; FilterType type { FilterType::kFilterLpf2p }; - CCMap cutoffCC { Default::filterCutoffCC }; + CCMap cutoffCC { Default::filterCutoffCC }; CCMap resonanceCC { Default::filterResonanceCC }; CCMap gainCC { Default::filterGainCC }; }; From 4e58a945a8c7f83e5a02f5e0f2979466d8058dd8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 12:02:47 +0200 Subject: [PATCH 259/445] Parse filter/eq modulation targets --- src/sfizz/Region.cpp | 7 +++++++ src/sfizz/modulations/ModId.cpp | 12 ++++++++++++ src/sfizz/modulations/ModId.h | 6 ++++++ src/sfizz/modulations/ModKey.cpp | 12 ++++++++++++ 4 files changed, 37 insertions(+) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index a6cea577..e0dfa31f 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -585,6 +585,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) return false; + processGenericCc(opcode, Default::filterCutoffModRange, ModKey::createNXYZ(ModId::FilCutoff, id, filterIndex)); setValueFromOpcode( opcode, filters[filterIndex].cutoffCC[opcode.parameters.back()], @@ -598,6 +599,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) return false; + processGenericCc(opcode, Default::filterResonanceModRange, ModKey::createNXYZ(ModId::FilResonance, id, filterIndex)); setValueFromOpcode( opcode, filters[filterIndex].resonanceCC[opcode.parameters.back()], @@ -656,6 +658,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) return false; + processGenericCc(opcode, Default::filterGainModRange, ModKey::createNXYZ(ModId::FilGain, id, filterIndex)); setValueFromOpcode( opcode, filters[filterIndex].gainCC[opcode.parameters.back()], @@ -688,6 +691,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) return false; + setValueFromOpcode(opcode, equalizers[eqNumber - 1].bandwidth, Default::eqBandwidthRange); } break; @@ -699,6 +703,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) return false; + processGenericCc(opcode, Default::eqBandwidthModRange, ModKey::createNXYZ(ModId::EqBandwidth, id, eqNumber)); setValueFromOpcode(opcode, equalizers[eqNumber - 1].bandwidthCC[opcode.parameters.back()], Default::eqBandwidthModRange); } break; @@ -720,6 +725,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) return false; + processGenericCc(opcode, Default::eqFrequencyModRange, ModKey::createNXYZ(ModId::EqFrequency, id, eqNumber)); setValueFromOpcode(opcode, equalizers[eqNumber - 1].frequencyCC[opcode.parameters.back()], Default::eqFrequencyModRange); } break; @@ -754,6 +760,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) return false; + processGenericCc(opcode, Default::eqGainModRange, ModKey::createNXYZ(ModId::EqGain, id, eqNumber)); setValueFromOpcode(opcode, equalizers[eqNumber - 1].gainCC[opcode.parameters.back()], Default::eqGainModRange); } break; diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index a75c648c..4ee37055 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -44,6 +44,18 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice|kModIsAdditive; case ModId::Volume: return kModIsPerVoice|kModIsAdditive; + case ModId::FilGain: + return kModIsPerVoice|kModIsAdditive; + case ModId::FilCutoff: + return kModIsPerVoice|kModIsAdditive; + case ModId::FilResonance: + return kModIsPerVoice|kModIsMultiplicative; + case ModId::EqGain: + return kModIsPerVoice|kModIsAdditive; + case ModId::EqFrequency: + return kModIsPerVoice|kModIsAdditive; + case ModId::EqBandwidth: + return kModIsPerVoice|kModIsAdditive; // unknown default: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index 8f18679a..7ab96ee9 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -37,6 +37,12 @@ enum class ModId : int { Position, Pitch, Volume, + FilGain, + FilCutoff, + FilResonance, + EqGain, + EqFrequency, + EqBandwidth, _TargetsEnd, // [/targets] -------------------------------------------------------------- diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index aa4dd1cd..8aa40a9d 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -87,6 +87,18 @@ std::string ModKey::toString() const return absl::StrCat("Pitch {", region_.number(), "}"); case ModId::Volume: return absl::StrCat("Volume {", region_.number(), "}"); + case ModId::FilGain: + return absl::StrCat("FilterGain {", region_.number(), "N=", params_.N, "}"); + case ModId::FilCutoff: + return absl::StrCat("FilterCutoff {", region_.number(), "N=", params_.N, "}"); + case ModId::FilResonance: + return absl::StrCat("FilterResonance {", region_.number(), "N=", params_.N, "}"); + case ModId::EqGain: + return absl::StrCat("EqGain {", region_.number(), "N=", params_.N, "}"); + case ModId::EqFrequency: + return absl::StrCat("EqFrequency {", region_.number(), "N=", params_.N, "}"); + case ModId::EqBandwidth: + return absl::StrCat("EqBandwitdth {", region_.number(), "N=", params_.N, "}"); default: return {}; From dc4f59107bac4bed0ea5218b94cf6f01a0c91323 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 13:42:20 +0200 Subject: [PATCH 260/445] Remove the EQ/Filter pools Voices hold their own tentative eq and filters that get initialized upon starting the voice depending on how many there are in the region --- src/sfizz/EQPool.cpp | 107 +++++++++------------------------------ src/sfizz/EQPool.h | 86 ++++--------------------------- src/sfizz/FilterPool.cpp | 107 +++++++++------------------------------ src/sfizz/FilterPool.h | 87 ++++--------------------------- src/sfizz/Resources.h | 7 +-- src/sfizz/Voice.cpp | 72 ++++++++++++++------------ src/sfizz/Voice.h | 6 ++- 7 files changed, 111 insertions(+), 361 deletions(-) diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index cc608a12..7e42546b 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -4,22 +4,23 @@ #include "SIMDHelpers.h" #include "SwapAndPop.h" -sfz::EQHolder::EQHolder(const MidiState& state) -:midiState(state) +sfz::EQHolder::EQHolder(const Resources& resources) +: resources(resources) { - + eq = absl::make_unique(); + eq->init(config::defaultSampleRate); } void sfz::EQHolder::reset() { - eq.clear(); + eq->clear(); } void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels, float velocity) { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - eq.setType(description.type); - eq.setChannels(numChannels); + eq->setType(description.type); + eq->setChannels(numChannels); this->description = &description; // Setup the base values @@ -28,29 +29,29 @@ void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels baseGain = description.gain + velocity * description.vel2gain; // Setup the modulated values - lastFrequency = baseFrequency; + float lastFrequency = baseFrequency; for (const auto& mod : description.frequencyCC) - lastFrequency += midiState.getCCValue(mod.cc) * mod.data; + lastFrequency += resources.midiState.getCCValue(mod.cc) * mod.data; lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); - lastBandwidth = baseBandwidth; + float lastBandwidth = baseBandwidth; for (const auto& mod : description.bandwidthCC) - lastBandwidth += midiState.getCCValue(mod.cc) * mod.data; + lastBandwidth += resources.midiState.getCCValue(mod.cc) * mod.data; lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); - lastGain = baseGain; + float lastGain = baseGain; for (const auto& mod : description.gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; + lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); // Initialize the EQ - eq.prepare(lastFrequency, lastBandwidth, lastGain); + eq->prepare(lastFrequency, lastBandwidth, lastGain); } void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numFrames) { auto justCopy = [&]() { - for (unsigned channelIdx = 0; channelIdx < eq.channels(); channelIdx++) + for (unsigned channelIdx = 0; channelIdx < eq->channels(); channelIdx++) copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); }; @@ -61,19 +62,19 @@ void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numF // TODO: Once the midistate envelopes are done, add modulation in there! // For now we take the last value - lastFrequency = baseFrequency; + float lastFrequency = baseFrequency; for (const auto& mod : description->frequencyCC) - lastFrequency += midiState.getCCValue(mod.cc) * mod.data; + lastFrequency += resources.midiState.getCCValue(mod.cc) * mod.data; lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); - lastBandwidth = baseBandwidth; + float lastBandwidth = baseBandwidth; for (const auto& mod : description->bandwidthCC) - lastBandwidth += midiState.getCCValue(mod.cc) * mod.data; + lastBandwidth += resources.midiState.getCCValue(mod.cc) * mod.data; lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); - lastGain = baseGain; + float lastGain = baseGain; for (const auto& mod : description->gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; + lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); if (lastGain == 0.0f) { @@ -81,70 +82,10 @@ void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numF return; } - eq.process(inputs, outputs, lastFrequency, lastBandwidth, lastGain, numFrames); -} -float sfz::EQHolder::getLastFrequency() const -{ - return lastFrequency; -} -float sfz::EQHolder::getLastBandwidth() const -{ - return lastBandwidth; -} -float sfz::EQHolder::getLastGain() const -{ - return lastGain; + eq->process(inputs, outputs, lastFrequency, lastBandwidth, lastGain, numFrames); } + void sfz::EQHolder::setSampleRate(float sampleRate) { - eq.init(static_cast(sampleRate)); -} - -sfz::EQPool::EQPool(const MidiState& state, int numEQs) -: midiState(state) -{ - setnumEQs(numEQs); -} - -sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, unsigned numChannels, float velocity) -{ - const std::unique_lock lock { eqGuard, std::try_to_lock }; - if (!lock.owns_lock()) - return {}; - - auto eq = absl::c_find_if(eqs, [](const EQHolderPtr& holder) { - return holder.use_count() == 1; - }); - - if (eq == eqs.end()) - return {}; - - (**eq).setup(description, numChannels, velocity); - return *eq; -} - -size_t sfz::EQPool::getActiveEQs() const -{ - return absl::c_count_if(eqs, [](const EQHolderPtr& holder) { - return holder.use_count() > 1; - }); -} - -size_t sfz::EQPool::setnumEQs(size_t numEQs) -{ - const std::lock_guard eqLock { eqGuard }; - - swapAndPopAll(eqs, [](sfz::EQHolderPtr& eq) { return eq.use_count() == 1; }); - - for (size_t i = eqs.size(); i < numEQs; ++i) { - eqs.emplace_back(std::make_shared(midiState)); - eqs.back()->setSampleRate(sampleRate); - } - - return eqs.size(); -} -void sfz::EQPool::setSampleRate(float sampleRate) -{ - for (auto& eq: eqs) - eq->setSampleRate(sampleRate); + eq->init(static_cast(sampleRate)); } diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index d1f7cebb..5b61c096 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -1,7 +1,7 @@ #pragma once #include "SfzFilter.h" #include "EQDescription.h" -#include "MidiState.h" +#include "Resources.h" #include "utility/SpinMutex.h" #include #include @@ -14,7 +14,7 @@ class EQHolder { public: EQHolder() = delete; - EQHolder(const MidiState& state); + EQHolder(const Resources& resources); /** * @brief Setup a new EQ based on an EQ description. * @@ -31,94 +31,26 @@ public: * @param numFrames */ void process(const float** inputs, float** outputs, unsigned numFrames); - /** - * @brief Returns the last value of the frequency for the EQ - * - * @return float - */ - float getLastFrequency() const; - /** - * @brief Returns the last value of the bandwitdh for the EQ - * - * @return float - */ - float getLastBandwidth() const; - /** - * @brief Returns the last value of the gain for the EQ - * - * @return float - */ - float getLastGain() const; /** * @brief Set the sample rate for the EQ * * @param sampleRate */ void setSampleRate(float sampleRate); -private: /** - * Reset the filter. Is called internally when using setup(). + * Reset the filter. */ void reset(); - const MidiState& midiState; +private: + const Resources& resources; const EQDescription* description; - FilterEq eq; + std::unique_ptr eq; float baseBandwidth { Default::eqBandwidth }; float baseFrequency { Default::eqFrequency1 }; float baseGain { Default::eqGain }; - float lastBandwidth { Default::eqBandwidth }; - float lastFrequency { Default::eqFrequency1 }; - float lastGain { Default::eqGain }; + ModMatrix::TargetId eqGainTarget; + ModMatrix::TargetId eqFrequencyTarget; + ModMatrix::TargetId eqBandwidthTarget; }; -using EQHolderPtr = std::shared_ptr; - -class EQPool -{ -public: - EQPool() = delete; - /** - * @brief Construct a new EQPool object - * - * @param state the associated midi state - * @param numEQs the number of inactive EQs to hold in the pool - */ - EQPool(const MidiState& state, int numEQs = config::filtersInPool); - /** - * @brief Get an EQ object to use in Voices - * - * @param description the filter description to bind to the EQ - * @param numChannels the number of channels for the EQ - * @param velocity the triggering note velocity/value - * @return EQHolderPtr release this when done with the filter; no deallocation will be done - */ - EQHolderPtr getEQ(const EQDescription& description, unsigned numChannels, float velocity); - /** - * @brief Get the number of active EQs - * - * @return size_t - */ - size_t getActiveEQs() const; - /** - * @brief Set the number of EQs in the pool. This function may sleep and should be called from a background thread. - * No EQs will be distributed during the reallocation of EQs. Existing running EQs are kept. If the target - * number of EQs is less that the number of active EQs, the function will not remove them and you may need - * to call it again after existing EQs have run out. - * - * @param numEQs - * @return size_t the actual number of EQs in the pool - */ - size_t setnumEQs(size_t numEQs); - /** - * @brief Set the sample rate for all EQs - * - * @param sampleRate - */ - void setSampleRate(float sampleRate); -private: - SpinMutex eqGuard; - float sampleRate { config::defaultSampleRate }; - const MidiState& midiState; - std::vector eqs; -}; } diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 70811320..de271a06 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -5,15 +5,16 @@ #include #include -sfz::FilterHolder::FilterHolder(const MidiState& midiState) -: midiState(midiState) +sfz::FilterHolder::FilterHolder(const Resources& resources) +: resources(resources) { - + filter = absl::make_unique(); + filter->init(config::defaultSampleRate); } void sfz::FilterHolder::reset() { - filter.clear(); + filter->clear(); } void sfz::FilterHolder::setup(const FilterDescription& description, unsigned numChannels, int noteNumber, float velocity) @@ -21,8 +22,8 @@ void sfz::FilterHolder::setup(const FilterDescription& description, unsigned num ASSERT(velocity >= 0.0f && velocity <= 1.0f); this->description = &description; - filter.setType(description.type); - filter.setChannels(numChannels); + filter->setType(description.type); + filter->setChannels(numChannels); // Setup the base values baseCutoff = description.cutoff; @@ -40,29 +41,29 @@ void sfz::FilterHolder::setup(const FilterDescription& description, unsigned num baseResonance = description.resonance; // Setup the modulated values - lastCutoff = baseCutoff; + float lastCutoff = baseCutoff; for (const auto& mod : description.cutoffCC) - lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data); + lastCutoff *= centsFactor(resources.midiState.getCCValue(mod.cc) * mod.data); lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); - lastResonance = baseResonance; + float lastResonance = baseResonance; for (const auto& mod : description.resonanceCC) - lastResonance += midiState.getCCValue(mod.cc) * mod.data; + lastResonance += resources.midiState.getCCValue(mod.cc) * mod.data; lastResonance = Default::filterResonanceRange.clamp(lastResonance); - lastGain = baseGain; + float lastGain = baseGain; for (const auto& mod : description.gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; + lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); // Initialize the filter - filter.prepare(lastCutoff, lastResonance, lastGain); + filter->prepare(lastCutoff, lastResonance, lastGain); } void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned numFrames) { if (description == nullptr) { - for (unsigned channelIdx = 0; channelIdx < filter.channels(); channelIdx++) + for (unsigned channelIdx = 0; channelIdx < filter->channels(); channelIdx++) copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); return; } @@ -70,88 +71,26 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned // TODO: Once the midistate envelopes are done, add modulation in there! // For now we take the last value // TODO: the template deduction could be automatic here? - lastCutoff = baseCutoff; + float lastCutoff = baseCutoff; for (const auto& mod : description->cutoffCC) - lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data); + lastCutoff *= centsFactor(resources.midiState.getCCValue(mod.cc) * mod.data); lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); - lastResonance = baseResonance; + float lastResonance = baseResonance; for (const auto& mod : description->resonanceCC) - lastResonance += midiState.getCCValue(mod.cc) * mod.data; + lastResonance += resources.midiState.getCCValue(mod.cc) * mod.data; lastResonance = Default::filterResonanceRange.clamp(lastResonance); - lastGain = baseGain; + float lastGain = baseGain; for (const auto& mod : description->gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; + lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); - filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); + filter->process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); } -float sfz::FilterHolder::getLastCutoff() const -{ - return lastCutoff; -} -float sfz::FilterHolder::getLastResonance() const -{ - return lastResonance; -} -float sfz::FilterHolder::getLastGain() const -{ - return lastGain; -} - -sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) -: midiState(state) -{ - setNumFilters(numFilters); -} - -sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, unsigned numChannels, int noteNumber, float velocity) -{ - const std::unique_lock lock { filterGuard, std::try_to_lock }; - if (!lock.owns_lock()) - return {}; - - auto filter = absl::c_find_if(filters, [](const FilterHolderPtr& holder) { - return holder.use_count() == 1; - }); - - if (filter == filters.end()) - return {}; - - (**filter).setup(description, numChannels, noteNumber, velocity); - return *filter; -} - -size_t sfz::FilterPool::getActiveFilters() const -{ - return absl::c_count_if(filters, [](const FilterHolderPtr& holder) { - return holder.use_count() > 1; - }); -} - -size_t sfz::FilterPool::setNumFilters(size_t numFilters) -{ - const std::lock_guard filterLock { filterGuard }; - - swapAndPopAll(filters, [](sfz::FilterHolderPtr& filter) { return filter.use_count() == 1; }); - - for (size_t i = filters.size(); i < numFilters; ++i) { - filters.emplace_back(std::make_shared(midiState)); - filters.back()->setSampleRate(sampleRate); - } - - return filters.size(); -} - -void sfz::FilterPool::setSampleRate(float sampleRate) -{ - for (auto& filter: filters) - filter->setSampleRate(sampleRate); -} void sfz::FilterHolder::setSampleRate(float sampleRate) { - filter.init(static_cast(sampleRate)); + filter->init(static_cast(sampleRate)); } diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index 197eb8ad..c926f196 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -1,7 +1,7 @@ #pragma once #include "SfzFilter.h" #include "FilterDescription.h" -#include "MidiState.h" +#include "Resources.h" #include "Defaults.h" #include "utility/SpinMutex.h" #include @@ -15,7 +15,7 @@ class FilterHolder { public: FilterHolder() = delete; - FilterHolder(const MidiState& state); + FilterHolder(const Resources& resources); /** * @brief Setup a new filter based on a filter description, and a triggering note parameters. * @@ -33,97 +33,28 @@ public: * @param numFrames */ void process(const float** inputs, float** outputs, unsigned numFrames); - /** - * @brief Returns the last value of the cutoff for the filter - * - * @return float - */ - float getLastCutoff() const; - /** - * @brief Returns the last value of the resonance for the filter - * - * @return float - */ - float getLastResonance() const; - /** - * @brief Returns the last value of the gain for the filter - * - * @return float - */ - float getLastGain() const; /** * @brief Set the sample rate for a filter * * @param sampleRate */ void setSampleRate(float sampleRate); -private: /** - * Reset the filter. Is called internally when using setup(). + * Reset the filter. */ void reset(); - const MidiState& midiState; +private: + const Resources& resources; const FilterDescription* description; - Filter filter; + std::unique_ptr filter; float baseCutoff { Default::filterCutoff }; float baseResonance { Default::filterResonance }; float baseGain { Default::filterGain }; - float lastCutoff { Default::filterCutoff }; - float lastResonance { Default::filterResonance }; - float lastGain { Default::filterGain }; + ModMatrix::TargetId filterGainTarget; + ModMatrix::TargetId filterCutoffTarget; + ModMatrix::TargetId filterResonanceTarget; using filterRandomDist = std::uniform_int_distribution; filterRandomDist dist { 0, sfz::Default::filterRandom }; }; -using FilterHolderPtr = std::shared_ptr; - -class FilterPool -{ -public: - FilterPool() = delete; - /** - * @brief Construct a new Filter Pool object - * - * @param state the associated midi state - * @param numFilters the number of inactive filters to hold in the pool - */ - FilterPool(const MidiState& state, int numFilters = config::filtersInPool); - /** - * @brief Get a filter object to use in Voices - * - * @param description the filter description to bind to the filter - * @param numChannels the number of channels in the underlying filter - * @param noteNumber the triggering note number - * @param velocity the triggering note velocity - * @return FilterHolderPtr release this when done with the filter; no deallocation will be done - */ - FilterHolderPtr getFilter(const FilterDescription& description, unsigned numChannels, int noteNumber = static_cast(Default::filterKeycenter), float velocity = 0); - /** - * @brief Get the number of active filters - * - * @return size_t - */ - size_t getActiveFilters() const; - /** - * @brief Set the number of filters in the pool. This function may sleep and should be called from a background thread. - * No filters will be distributed during the reallocation of filters. Existing running filters are kept. If the target - * number of filters is less that the number of active filters, the function will not remove them and you may need - * to call it again after existing filters have run out. - * - * @param numFilters - * @return size_t the actual number of filters in the pool - */ - size_t setNumFilters(size_t numFilters); - /** - * @brief Set the sample rate for all filters - * - * @param sampleRate - */ - void setSampleRate(float sampleRate); -private: - SpinMutex filterGuard; - float sampleRate { config::defaultSampleRate }; - const MidiState& midiState; - std::vector filters; -}; } diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 645482cd..577ff756 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -6,10 +6,9 @@ #pragma once #include "SynthConfig.h" +#include "MidiState.h" #include "FilePool.h" #include "BufferPool.h" -#include "FilterPool.h" -#include "EQPool.h" #include "Logger.h" #include "Wavetables.h" #include "Curve.h" @@ -29,8 +28,6 @@ struct Resources Logger logger; CurveSet curves; FilePool filePool { logger }; - FilterPool filterPool { midiState }; - EQPool eqPool { midiState }; WavetablePool wavePool; Tuning tuning; absl::optional stretch; @@ -39,8 +36,6 @@ struct Resources void setSampleRate(float samplerate) { midiState.setSampleRate(samplerate); - filterPool.setSampleRate(samplerate); - eqPool.setSampleRate(samplerate); modMatrix.setSampleRate(samplerate); } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index cb604633..6cc473d0 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -23,8 +23,11 @@ sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources) : id{voiceNumber}, stateListener(nullptr), resources(resources) { - filters.reserve(config::filtersPerVoice); - equalizers.reserve(config::eqsPerVoice); + for (unsigned i = 0; i < config::filtersPerVoice; ++i) + filters.emplace_back(resources); + + for (unsigned i = 0; i < config::eqsPerVoice; ++i) + equalizers.emplace_back(resources); for (WavetableOscillator& osc : waveOscillators) osc.init(sampleRate); @@ -118,21 +121,13 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event gainSmoother.reset(); resetCrossfades(); - // Check that we can handle the number of filters; filters should be cleared here - ASSERT((filters.capacity() - filters.size()) >= region->filters.size()); - ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size()); - const unsigned numChannels = region->isStereo() ? 2 : 1; - for (auto& filter: region->filters) { - auto newFilter = resources.filterPool.getFilter(filter, numChannels, triggerEvent.number, triggerEvent.value); - if (newFilter) - filters.push_back(newFilter); + for (unsigned i = 0; i < region->filters.size(); ++i) { + filters[i].setup(region->filters[i], numChannels, triggerEvent.number, triggerEvent.value); } - for (auto& eq: region->equalizers) { - auto newEQ = resources.eqPool.getEQ(eq, numChannels, triggerEvent.value); - if (newEQ) - equalizers.push_back(newEQ); + for (unsigned i = 0; i < region->equalizers.size(); ++i) { + equalizers[i].setup(region->equalizers[i], numChannels, triggerEvent.value); } sourcePosition = region->getOffset(); @@ -253,6 +248,12 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept for (auto& lfo : lfos) lfo->setSampleRate(sampleRate); + for (auto& filter : filters) + filter.setSampleRate(sampleRate); + + for (auto& eq : equalizers) + eq.setSampleRate(sampleRate); + powerFollower.setSampleRate(sampleRate); } @@ -498,12 +499,12 @@ void sfz::Voice::filterStageMono(AudioSpan buffer) noexcept const auto leftBuffer = buffer.getSpan(0); const float* inputChannel[1] { leftBuffer.data() }; float* outputChannel[1] { leftBuffer.data() }; - for (auto& filter : filters) { - filter->process(inputChannel, outputChannel, numSamples); + for (unsigned i = 0; i < region->filters.size(); ++i) { + filters[i].process(inputChannel, outputChannel, numSamples); } - for (auto& eq : equalizers) { - eq->process(inputChannel, outputChannel, numSamples); + for (unsigned i = 0; i < region->equalizers.size(); ++i) { + equalizers[i].process(inputChannel, outputChannel, numSamples); } } @@ -517,12 +518,12 @@ void sfz::Voice::filterStageStereo(AudioSpan buffer) noexcept const float* inputChannels[2] { leftBuffer.data(), rightBuffer.data() }; float* outputChannels[2] { leftBuffer.data(), rightBuffer.data() }; - for (auto& filter : filters) { - filter->process(inputChannels, outputChannels, numSamples); + for (unsigned i = 0; i < region->filters.size(); ++i) { + filters[i].process(inputChannels, outputChannels, numSamples); } - for (auto& eq : equalizers) { - eq->process(inputChannels, outputChannels, numSamples); + for (unsigned i = 0; i < region->equalizers.size(); ++i) { + equalizers[i].process(inputChannels, outputChannels, numSamples); } } @@ -611,7 +612,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept sourcePosition = indices->back(); floatPositionOffset = coeffs->back(); -#if 0 +#if 1 ASSERT(!hasNanInf(buffer.getConstSpan(0))); ASSERT(!hasNanInf(buffer.getConstSpan(1))); SFIZZ_CHECK(isReasonableAudio(buffer.getConstSpan(0))); @@ -731,8 +732,11 @@ void sfz::Voice::reset() noexcept powerFollower.clear(); - filters.clear(); - equalizers.clear(); + for (auto& filter : filters) + filter.reset(); + + for (auto& eq : equalizers) + eq.reset(); removeVoiceFromRing(); } @@ -776,16 +780,22 @@ uint32_t sfz::Voice::getSourcePosition() const noexcept void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters) { - // There are filters in there, this call is unexpected - ASSERT(filters.size() == 0); - filters.reserve(numFilters); + if (numFilters == filters.size()) + return; + + filters.clear(); + for (unsigned i = 0; i < numFilters; ++i) + filters.emplace_back(resources); } void sfz::Voice::setMaxEQsPerVoice(size_t numFilters) { - // There are filters in there, this call is unexpected - ASSERT(equalizers.size() == 0); - equalizers.reserve(numFilters); + if (numFilters == equalizers.size()) + return; + + equalizers.clear(); + for (unsigned i = 0; i < numFilters; ++i) + equalizers.emplace_back(resources); } void sfz::Voice::setMaxLFOsPerVoice(size_t numLFOs) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 8833271a..8afe3a06 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -12,6 +12,8 @@ #include "Region.h" #include "AudioBuffer.h" #include "Resources.h" +#include "FilterPool.h" +#include "EQPool.h" #include "Smoothers.h" #include "AudioSpan.h" #include "LeakDetector.h" @@ -454,8 +456,8 @@ private: Resources& resources; - std::vector filters; - std::vector equalizers; + std::vector filters; + std::vector equalizers; std::vector> lfos; std::vector> flexEGs; From bebd398e3976e2e1763a0f29736d98fa63d0e4fa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 13:56:37 +0200 Subject: [PATCH 261/445] Setup the filters and EQ from the region --- src/sfizz/EQPool.cpp | 22 ++++++++++++---------- src/sfizz/EQPool.h | 12 ++++++------ src/sfizz/FilterPool.cpp | 29 +++++++++++++++-------------- src/sfizz/FilterPool.h | 12 ++++++------ src/sfizz/Voice.cpp | 5 ++--- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index 7e42546b..20267647 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -16,31 +16,33 @@ void sfz::EQHolder::reset() eq->clear(); } -void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels, float velocity) +void sfz::EQHolder::setup(const Region& region, unsigned eqId, float velocity) { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - eq->setType(description.type); - eq->setChannels(numChannels); - this->description = &description; + ASSERT(eqId < region.equalizers.size()); + + this->description = ®ion.equalizers[eqId]; + eq->setType(description->type); + eq->setChannels(region.isStereo() ? 2 : 1); // Setup the base values - baseFrequency = description.frequency + velocity * description.vel2frequency; - baseBandwidth = description.bandwidth; - baseGain = description.gain + velocity * description.vel2gain; + baseFrequency = description->frequency + velocity * description->vel2frequency; + baseBandwidth = description->bandwidth; + baseGain = description->gain + velocity * description->vel2gain; // Setup the modulated values float lastFrequency = baseFrequency; - for (const auto& mod : description.frequencyCC) + for (const auto& mod : description->frequencyCC) lastFrequency += resources.midiState.getCCValue(mod.cc) * mod.data; lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); float lastBandwidth = baseBandwidth; - for (const auto& mod : description.bandwidthCC) + for (const auto& mod : description->bandwidthCC) lastBandwidth += resources.midiState.getCCValue(mod.cc) * mod.data; lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); float lastGain = baseGain; - for (const auto& mod : description.gainCC) + for (const auto& mod : description->gainCC) lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index 5b61c096..dc4b65fa 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -1,6 +1,6 @@ #pragma once #include "SfzFilter.h" -#include "EQDescription.h" +#include "Region.h" #include "Resources.h" #include "utility/SpinMutex.h" #include @@ -16,13 +16,13 @@ public: EQHolder() = delete; EQHolder(const Resources& resources); /** - * @brief Setup a new EQ based on an EQ description. + * @brief Setup a new EQ from a region and an index * - * @param description the EQ description - * @param numChannels the number of channels for the EQ - * @param description the triggering velocity/value + * @param description the region from which we take the EQ + * @param eqId the EQ index in the region + * @param description the triggering velocity/value */ - void setup(const EQDescription& description, unsigned numChannels, float velocity); + void setup(const Region& region, unsigned eqId, float velocity); /** * @brief Process a block of stereo inputs * diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index de271a06..5362e8a3 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -17,42 +17,43 @@ void sfz::FilterHolder::reset() filter->clear(); } -void sfz::FilterHolder::setup(const FilterDescription& description, unsigned numChannels, int noteNumber, float velocity) +void sfz::FilterHolder::setup(const Region& region, unsigned filterId, int noteNumber, float velocity) { ASSERT(velocity >= 0.0f && velocity <= 1.0f); + ASSERT(filterId < region.filters.size()); - this->description = &description; - filter->setType(description.type); - filter->setChannels(numChannels); + this->description = ®ion.filters[filterId]; + filter->setType(description->type); + filter->setChannels(region.isStereo() ? 2 : 1); // Setup the base values - baseCutoff = description.cutoff; - if (description.random != 0) { - dist.param(filterRandomDist::param_type(0, description.random)); + baseCutoff = description->cutoff; + if (description->random != 0) { + dist.param(filterRandomDist::param_type(0, description->random)); baseCutoff *= centsFactor(dist(Random::randomGenerator)); } - const auto keytrack = description.keytrack * (noteNumber - description.keycenter); + const auto keytrack = description->keytrack * (noteNumber - description->keycenter); baseCutoff *= centsFactor(keytrack); - const auto veltrack = static_cast(description.veltrack) * velocity; + const auto veltrack = static_cast(description->veltrack) * velocity; baseCutoff *= centsFactor(veltrack); baseCutoff = Default::filterCutoffRange.clamp(baseCutoff); - baseGain = description.gain; - baseResonance = description.resonance; + baseGain = description->gain; + baseResonance = description->resonance; // Setup the modulated values float lastCutoff = baseCutoff; - for (const auto& mod : description.cutoffCC) + for (const auto& mod : description->cutoffCC) lastCutoff *= centsFactor(resources.midiState.getCCValue(mod.cc) * mod.data); lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); float lastResonance = baseResonance; - for (const auto& mod : description.resonanceCC) + for (const auto& mod : description->resonanceCC) lastResonance += resources.midiState.getCCValue(mod.cc) * mod.data; lastResonance = Default::filterResonanceRange.clamp(lastResonance); float lastGain = baseGain; - for (const auto& mod : description.gainCC) + for (const auto& mod : description->gainCC) lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index c926f196..b3709cea 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -1,6 +1,6 @@ #pragma once #include "SfzFilter.h" -#include "FilterDescription.h" +#include "Region.h" #include "Resources.h" #include "Defaults.h" #include "utility/SpinMutex.h" @@ -19,12 +19,12 @@ public: /** * @brief Setup a new filter based on a filter description, and a triggering note parameters. * - * @param description the filter description - * @param numChannels the number of channels - * @param noteNumber the triggering note number - * @param velocity the triggering note velocity/value + * @param description the region from which we take the filter + * @param filterId the filter index in the region + * @param noteNumber the triggering note number + * @param velocity the triggering note velocity/value */ - void setup(const FilterDescription& description, unsigned numChannels, int noteNumber = static_cast(Default::filterKeycenter), float velocity = 0); + void setup(const Region& region, unsigned filterId, int noteNumber = static_cast(Default::filterKeycenter), float velocity = 0); /** * @brief Process a block of stereo inputs * diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 6cc473d0..d080db37 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -121,13 +121,12 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event gainSmoother.reset(); resetCrossfades(); - const unsigned numChannels = region->isStereo() ? 2 : 1; for (unsigned i = 0; i < region->filters.size(); ++i) { - filters[i].setup(region->filters[i], numChannels, triggerEvent.number, triggerEvent.value); + filters[i].setup(*region, i, triggerEvent.number, triggerEvent.value); } for (unsigned i = 0; i < region->equalizers.size(); ++i) { - equalizers[i].setup(region->equalizers[i], numChannels, triggerEvent.value); + equalizers[i].setup(*region, i, triggerEvent.value); } sourcePosition = region->getOffset(); From 0f74039b39344181739c2aec6a2a092f9d788e46 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 13:59:55 +0200 Subject: [PATCH 262/445] Make find methods const in the mod matrix --- src/sfizz/modulations/ModMatrix.cpp | 4 ++-- src/sfizz/modulations/ModMatrix.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 4533d156..d7ddc444 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -168,7 +168,7 @@ ModMatrix::TargetId ModMatrix::registerTarget(const ModKey& key) return id; } -ModMatrix::SourceId ModMatrix::findSource(const ModKey& key) +ModMatrix::SourceId ModMatrix::findSource(const ModKey& key) const { Impl& impl = *impl_; @@ -179,7 +179,7 @@ ModMatrix::SourceId ModMatrix::findSource(const ModKey& key) return SourceId(it->second); } -ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key) +ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key) const { Impl& impl = *impl_; diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index b0f9f58f..0f0fb471 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -77,14 +77,14 @@ public: * * @param key source key */ - SourceId findSource(const ModKey& key); + SourceId findSource(const ModKey& key) const; /** * @brief Look up a target by key. * * @param key target key */ - TargetId findTarget(const ModKey& key); + TargetId findTarget(const ModKey& key) const; /** * @brief Connect a source and a destination inside the matrix. From f14548116070fe80157df5376620482048cc5b7b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 14:48:36 +0200 Subject: [PATCH 263/445] Use the mod matrix in filters and eqs --- src/sfizz/EQPool.cpp | 60 +++++++++++++++++++-------------- src/sfizz/EQPool.h | 10 +++--- src/sfizz/FilterPool.cpp | 50 +++++++++++++++++---------- src/sfizz/FilterPool.h | 10 +++--- src/sfizz/modulations/ModId.cpp | 2 +- 5 files changed, 78 insertions(+), 54 deletions(-) diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index 20267647..59cdecb5 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -4,7 +4,7 @@ #include "SIMDHelpers.h" #include "SwapAndPop.h" -sfz::EQHolder::EQHolder(const Resources& resources) +sfz::EQHolder::EQHolder(Resources& resources) : resources(resources) { eq = absl::make_unique(); @@ -46,45 +46,53 @@ void sfz::EQHolder::setup(const Region& region, unsigned eqId, float velocity) lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); + gainTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqGain, region.id, eqId)); + bandwidthTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqBandwidth, region.id, eqId)); + frequencyTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqFrequency, region.id, eqId)); + // Initialize the EQ + DBG(baseFrequency << " " << baseBandwidth << " " << baseGain); eq->prepare(lastFrequency, lastBandwidth, lastGain); } void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numFrames) { - auto justCopy = [&]() { + if (description == nullptr) { for (unsigned channelIdx = 0; channelIdx < eq->channels(); channelIdx++) copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); - }; - - if (description == nullptr) { - justCopy(); return; } - // TODO: Once the midistate envelopes are done, add modulation in there! - // For now we take the last value - float lastFrequency = baseFrequency; - for (const auto& mod : description->frequencyCC) - lastFrequency += resources.midiState.getCCValue(mod.cc) * mod.data; - lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); + ModMatrix& mm = resources.modMatrix; + auto frequencySpan = resources.bufferPool.getBuffer(numFrames); + auto bandwidthSpan = resources.bufferPool.getBuffer(numFrames); + auto gainSpan = resources.bufferPool.getBuffer(numFrames); - float lastBandwidth = baseBandwidth; - for (const auto& mod : description->bandwidthCC) - lastBandwidth += resources.midiState.getCCValue(mod.cc) * mod.data; - lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); - - float lastGain = baseGain; - for (const auto& mod : description->gainCC) - lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); - - if (lastGain == 0.0f) { - justCopy(); + if (!frequencySpan || !bandwidthSpan || !gainSpan) return; - } - eq->process(inputs, outputs, lastFrequency, lastBandwidth, lastGain, numFrames); + fill(*frequencySpan, baseFrequency); + if (float* mod = mm.getModulation(frequencyTarget)) + add(absl::Span(mod, numFrames), *frequencySpan); + + fill(*bandwidthSpan, baseBandwidth); + if (float* mod = mm.getModulation(bandwidthTarget)) + add(absl::Span(mod, numFrames), *bandwidthSpan); + + fill(*gainSpan, baseGain); + if (float* mod = mm.getModulation(gainTarget)) + add(absl::Span(mod, numFrames), *gainSpan); + + DBG(frequencySpan->back() << " " << bandwidthSpan->back() << " " << gainSpan->back()); + + eq->processModulated( + inputs, + outputs, + frequencySpan->data(), + bandwidthSpan->data(), + gainSpan->data(), + numFrames + ); } void sfz::EQHolder::setSampleRate(float sampleRate) diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index dc4b65fa..f284e69e 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -14,7 +14,7 @@ class EQHolder { public: EQHolder() = delete; - EQHolder(const Resources& resources); + EQHolder(Resources& resources); /** * @brief Setup a new EQ from a region and an index * @@ -42,15 +42,15 @@ public: */ void reset(); private: - const Resources& resources; + Resources& resources; const EQDescription* description; std::unique_ptr eq; float baseBandwidth { Default::eqBandwidth }; float baseFrequency { Default::eqFrequency1 }; float baseGain { Default::eqGain }; - ModMatrix::TargetId eqGainTarget; - ModMatrix::TargetId eqFrequencyTarget; - ModMatrix::TargetId eqBandwidthTarget; + ModMatrix::TargetId gainTarget; + ModMatrix::TargetId frequencyTarget; + ModMatrix::TargetId bandwidthTarget; }; } diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 5362e8a3..d60accad 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -5,7 +5,7 @@ #include #include -sfz::FilterHolder::FilterHolder(const Resources& resources) +sfz::FilterHolder::FilterHolder(Resources& resources) : resources(resources) { filter = absl::make_unique(); @@ -57,6 +57,11 @@ void sfz::FilterHolder::setup(const Region& region, unsigned filterId, int noteN lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; lastGain = Default::filterGainRange.clamp(lastGain); + ModMatrix& mm = resources.modMatrix; + gainTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilGain, region.id, filterId)); + cutoffTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilCutoff, region.id, filterId)); + resonanceTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilResonance, region.id, filterId)); + // Initialize the filter filter->prepare(lastCutoff, lastResonance, lastGain); } @@ -69,25 +74,36 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned return; } - // TODO: Once the midistate envelopes are done, add modulation in there! - // For now we take the last value - // TODO: the template deduction could be automatic here? - float lastCutoff = baseCutoff; - for (const auto& mod : description->cutoffCC) - lastCutoff *= centsFactor(resources.midiState.getCCValue(mod.cc) * mod.data); - lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); + ModMatrix& mm = resources.modMatrix; + auto cutoffSpan = resources.bufferPool.getBuffer(numFrames); + auto resonanceSpan = resources.bufferPool.getBuffer(numFrames); + auto gainSpan = resources.bufferPool.getBuffer(numFrames); - float lastResonance = baseResonance; - for (const auto& mod : description->resonanceCC) - lastResonance += resources.midiState.getCCValue(mod.cc) * mod.data; - lastResonance = Default::filterResonanceRange.clamp(lastResonance); + if (!cutoffSpan || !resonanceSpan || !gainSpan) + return; - float lastGain = baseGain; - for (const auto& mod : description->gainCC) - lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); + fill(*cutoffSpan, baseCutoff); + if (float* mod = mm.getModulation(cutoffTarget)) { + for (size_t i = 0; i < numFrames; ++i) + (*cutoffSpan)[i] *= centsFactor(mod[i]); + } - filter->process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); + fill(*resonanceSpan, baseResonance); + if (float* mod = mm.getModulation(resonanceTarget)) + add(absl::Span(mod, numFrames), *resonanceSpan); + + fill(*gainSpan, baseGain); + if (float* mod = mm.getModulation(gainTarget)) + add(absl::Span(mod, numFrames), *gainSpan); + + filter->processModulated( + inputs, + outputs, + cutoffSpan->data(), + resonanceSpan->data(), + gainSpan->data(), + numFrames + ); } diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index b3709cea..9e7f0522 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -15,7 +15,7 @@ class FilterHolder { public: FilterHolder() = delete; - FilterHolder(const Resources& resources); + FilterHolder(Resources& resources); /** * @brief Setup a new filter based on a filter description, and a triggering note parameters. * @@ -44,15 +44,15 @@ public: */ void reset(); private: - const Resources& resources; + Resources& resources; const FilterDescription* description; std::unique_ptr filter; float baseCutoff { Default::filterCutoff }; float baseResonance { Default::filterResonance }; float baseGain { Default::filterGain }; - ModMatrix::TargetId filterGainTarget; - ModMatrix::TargetId filterCutoffTarget; - ModMatrix::TargetId filterResonanceTarget; + ModMatrix::TargetId gainTarget; + ModMatrix::TargetId cutoffTarget; + ModMatrix::TargetId resonanceTarget; using filterRandomDist = std::uniform_int_distribution; filterRandomDist dist { 0, sfz::Default::filterRandom }; }; diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index 4ee37055..9d19e10e 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -49,7 +49,7 @@ int ModIds::flags(ModId id) noexcept case ModId::FilCutoff: return kModIsPerVoice|kModIsAdditive; case ModId::FilResonance: - return kModIsPerVoice|kModIsMultiplicative; + return kModIsPerVoice|kModIsAdditive; case ModId::EqGain: return kModIsPerVoice|kModIsAdditive; case ModId::EqFrequency: From 9aceecaa54e890e64368b85bd6b9d2f2985382b2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 14:49:34 +0200 Subject: [PATCH 264/445] The eq number did not act like the filter index --- src/sfizz/Region.cpp | 80 +++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 49 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index e0dfa31f..664d2e31 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -686,111 +686,93 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) // Performance parameters: EQ case hash("eq&_bw"): { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) - return false; - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) + const auto eqIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - setValueFromOpcode(opcode, equalizers[eqNumber - 1].bandwidth, Default::eqBandwidthRange); + setValueFromOpcode(opcode, equalizers[eqIndex].bandwidth, Default::eqBandwidthRange); } break; case hash("eq&_bw_oncc&"): // also eq&_bwcc& { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) - return false; - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) + const auto eqIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - processGenericCc(opcode, Default::eqBandwidthModRange, ModKey::createNXYZ(ModId::EqBandwidth, id, eqNumber)); - setValueFromOpcode(opcode, equalizers[eqNumber - 1].bandwidthCC[opcode.parameters.back()], Default::eqBandwidthModRange); + processGenericCc(opcode, Default::eqBandwidthModRange, ModKey::createNXYZ(ModId::EqBandwidth, id, eqIndex)); + setValueFromOpcode(opcode, equalizers[eqIndex].bandwidthCC[opcode.parameters.back()], Default::eqBandwidthModRange); } break; case hash("eq&_freq"): { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) + const auto eqIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) - return false; - setValueFromOpcode(opcode, equalizers[eqNumber - 1].frequency, Default::eqFrequencyRange); + setValueFromOpcode(opcode, equalizers[eqIndex].frequency, Default::eqFrequencyRange); } break; case hash("eq&_freq_oncc&"): // also eq&_freqcc& { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) - return false; - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) + const auto eqIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - processGenericCc(opcode, Default::eqFrequencyModRange, ModKey::createNXYZ(ModId::EqFrequency, id, eqNumber)); - setValueFromOpcode(opcode, equalizers[eqNumber - 1].frequencyCC[opcode.parameters.back()], Default::eqFrequencyModRange); + processGenericCc(opcode, Default::eqFrequencyModRange, ModKey::createNXYZ(ModId::EqFrequency, id, eqIndex)); + setValueFromOpcode(opcode, equalizers[eqIndex].frequencyCC[opcode.parameters.back()], Default::eqFrequencyModRange); } break; case hash("eq&_vel&freq"): { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) - return false; + const auto eqIndex = opcode.parameters.front() - 1; if (opcode.parameters[1] != 2) return false; // was eqN_vel3freq or something else than eqN_vel2freq - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - setValueFromOpcode(opcode, equalizers[eqNumber - 1].vel2frequency, Default::eqFrequencyModRange); + setValueFromOpcode(opcode, equalizers[eqIndex].vel2frequency, Default::eqFrequencyModRange); } break; case hash("eq&_gain"): { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) + const auto eqIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) - return false; - setValueFromOpcode(opcode, equalizers[eqNumber - 1].gain, Default::eqGainRange); + setValueFromOpcode(opcode, equalizers[eqIndex].gain, Default::eqGainRange); } break; case hash("eq&_gain_oncc&"): // also eq&_gaincc& { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) - return false; - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) + const auto eqIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - processGenericCc(opcode, Default::eqGainModRange, ModKey::createNXYZ(ModId::EqGain, id, eqNumber)); - setValueFromOpcode(opcode, equalizers[eqNumber - 1].gainCC[opcode.parameters.back()], Default::eqGainModRange); + processGenericCc(opcode, Default::eqGainModRange, ModKey::createNXYZ(ModId::EqGain, id, eqIndex)); + setValueFromOpcode(opcode, equalizers[eqIndex].gainCC[opcode.parameters.back()], Default::eqGainModRange); } break; case hash("eq&_vel&gain"): { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) - return false; + const auto eqIndex = opcode.parameters.front() - 1; if (opcode.parameters[1] != 2) return false; // was eqN_vel3gain or something else than eqN_vel2gain - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; - setValueFromOpcode(opcode, equalizers[eqNumber - 1].vel2gain, Default::eqGainModRange); + setValueFromOpcode(opcode, equalizers[eqIndex].vel2gain, Default::eqGainModRange); } break; case hash("eq&_type"): { - const auto eqNumber = opcode.parameters.front(); - if (eqNumber == 0) - return false; - if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs)) + const auto eqIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) return false; absl::optional ftype = FilterEq::typeFromName(opcode.value); if (ftype) - equalizers[eqNumber - 1].type = *ftype; + equalizers[eqIndex].type = *ftype; else { - equalizers[eqNumber - 1].type = EqType::kEqNone; + equalizers[eqIndex].type = EqType::kEqNone; DBG("Unknown EQ type: " << opcode.value); } } From fd3a08b7a4bfa9411dee802ca2fcd21c85a6a441 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 15:33:26 +0200 Subject: [PATCH 265/445] Prepare filters on first call --- src/sfizz/EQPool.cpp | 27 +++++++-------------------- src/sfizz/EQPool.h | 1 + src/sfizz/FilterPool.cpp | 29 +++++++++++------------------ src/sfizz/FilterPool.h | 1 + 4 files changed, 20 insertions(+), 38 deletions(-) diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index 59cdecb5..bca8c711 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -14,6 +14,7 @@ sfz::EQHolder::EQHolder(Resources& resources) void sfz::EQHolder::reset() { eq->clear(); + prepared = false; } void sfz::EQHolder::setup(const Region& region, unsigned eqId, float velocity) @@ -30,29 +31,12 @@ void sfz::EQHolder::setup(const Region& region, unsigned eqId, float velocity) baseBandwidth = description->bandwidth; baseGain = description->gain + velocity * description->vel2gain; - // Setup the modulated values - float lastFrequency = baseFrequency; - for (const auto& mod : description->frequencyCC) - lastFrequency += resources.midiState.getCCValue(mod.cc) * mod.data; - lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); - - float lastBandwidth = baseBandwidth; - for (const auto& mod : description->bandwidthCC) - lastBandwidth += resources.midiState.getCCValue(mod.cc) * mod.data; - lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); - - float lastGain = baseGain; - for (const auto& mod : description->gainCC) - lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); - gainTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqGain, region.id, eqId)); bandwidthTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqBandwidth, region.id, eqId)); frequencyTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqFrequency, region.id, eqId)); - // Initialize the EQ - DBG(baseFrequency << " " << baseBandwidth << " " << baseGain); - eq->prepare(lastFrequency, lastBandwidth, lastGain); + // Disables smoothing of the parameters on the first call + prepared = false; } void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numFrames) @@ -83,7 +67,10 @@ void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numF if (float* mod = mm.getModulation(gainTarget)) add(absl::Span(mod, numFrames), *gainSpan); - DBG(frequencySpan->back() << " " << bandwidthSpan->back() << " " << gainSpan->back()); + if (!prepared) { + eq->prepare(frequencySpan->front(), bandwidthSpan->front(), gainSpan->front()); + prepared = true; + } eq->processModulated( inputs, diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index f284e69e..30ce889a 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -48,6 +48,7 @@ private: float baseBandwidth { Default::eqBandwidth }; float baseFrequency { Default::eqFrequency1 }; float baseGain { Default::eqGain }; + bool prepared { false }; ModMatrix::TargetId gainTarget; ModMatrix::TargetId frequencyTarget; ModMatrix::TargetId bandwidthTarget; diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index d60accad..c5eadcef 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -15,6 +15,7 @@ sfz::FilterHolder::FilterHolder(Resources& resources) void sfz::FilterHolder::reset() { filter->clear(); + prepared = false; } void sfz::FilterHolder::setup(const Region& region, unsigned filterId, int noteNumber, float velocity) @@ -41,33 +42,20 @@ void sfz::FilterHolder::setup(const Region& region, unsigned filterId, int noteN baseGain = description->gain; baseResonance = description->resonance; - // Setup the modulated values - float lastCutoff = baseCutoff; - for (const auto& mod : description->cutoffCC) - lastCutoff *= centsFactor(resources.midiState.getCCValue(mod.cc) * mod.data); - lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); - - float lastResonance = baseResonance; - for (const auto& mod : description->resonanceCC) - lastResonance += resources.midiState.getCCValue(mod.cc) * mod.data; - lastResonance = Default::filterResonanceRange.clamp(lastResonance); - - float lastGain = baseGain; - for (const auto& mod : description->gainCC) - lastGain += resources.midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); - ModMatrix& mm = resources.modMatrix; gainTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilGain, region.id, filterId)); cutoffTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilCutoff, region.id, filterId)); resonanceTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilResonance, region.id, filterId)); - // Initialize the filter - filter->prepare(lastCutoff, lastResonance, lastGain); + // Disable smoothing of the parameters on the first call + prepared = false; } void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned numFrames) { + if (numFrames == 0) + return; + if (description == nullptr) { for (unsigned channelIdx = 0; channelIdx < filter->channels(); channelIdx++) copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); @@ -96,6 +84,11 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned if (float* mod = mm.getModulation(gainTarget)) add(absl::Span(mod, numFrames), *gainSpan); + if (!prepared) { + filter->prepare(cutoffSpan->front(), resonanceSpan->front(), gainSpan->front()); + prepared = true; + } + filter->processModulated( inputs, outputs, diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index 9e7f0522..f11ede24 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -53,6 +53,7 @@ private: ModMatrix::TargetId gainTarget; ModMatrix::TargetId cutoffTarget; ModMatrix::TargetId resonanceTarget; + bool prepared { false }; using filterRandomDist = std::uniform_int_distribution; filterRandomDist dist { 0, sfz::Default::filterRandom }; }; From f07000dd8804e42a38aa67615bf80b18c901008a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 16:07:52 +0200 Subject: [PATCH 266/445] Remove the cc maps --- src/sfizz/EQDescription.h | 3 -- src/sfizz/FilterDescription.h | 3 -- src/sfizz/Region.cpp | 18 -------- tests/ModulationsT.cpp | 18 ++++++++ tests/RegionT.cpp | 78 ----------------------------------- 5 files changed, 18 insertions(+), 102 deletions(-) diff --git a/src/sfizz/EQDescription.h b/src/sfizz/EQDescription.h index 161207df..c109a4a0 100644 --- a/src/sfizz/EQDescription.h +++ b/src/sfizz/EQDescription.h @@ -20,8 +20,5 @@ struct EQDescription float vel2frequency { Default::eqVel2frequency }; float vel2gain { Default::eqVel2gain }; EqType type { EqType::kEqPeak }; - CCMap bandwidthCC { Default::eqBandwidthCC }; - CCMap frequencyCC { Default::eqFrequencyCC }; - CCMap gainCC { Default::eqGainCC }; }; } diff --git a/src/sfizz/FilterDescription.h b/src/sfizz/FilterDescription.h index e21a9b58..f78b80d3 100644 --- a/src/sfizz/FilterDescription.h +++ b/src/sfizz/FilterDescription.h @@ -22,8 +22,5 @@ struct FilterDescription int veltrack { Default::filterVeltrack }; int random { Default::filterRandom }; FilterType type { FilterType::kFilterLpf2p }; - CCMap cutoffCC { Default::filterCutoffCC }; - CCMap resonanceCC { Default::filterResonanceCC }; - CCMap gainCC { Default::filterGainCC }; }; } diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 664d2e31..662ad1a6 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -586,11 +586,6 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; processGenericCc(opcode, Default::filterCutoffModRange, ModKey::createNXYZ(ModId::FilCutoff, id, filterIndex)); - setValueFromOpcode( - opcode, - filters[filterIndex].cutoffCC[opcode.parameters.back()], - Default::filterCutoffModRange - ); } break; case hash("resonance&_oncc&"): // also resonance_oncc&, resonance_cc&, resonance&_cc& @@ -600,11 +595,6 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; processGenericCc(opcode, Default::filterResonanceModRange, ModKey::createNXYZ(ModId::FilResonance, id, filterIndex)); - setValueFromOpcode( - opcode, - filters[filterIndex].resonanceCC[opcode.parameters.back()], - Default::filterResonanceModRange - ); } break; case hash("fil&_keytrack"): // also fil_keytrack @@ -659,11 +649,6 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; processGenericCc(opcode, Default::filterGainModRange, ModKey::createNXYZ(ModId::FilGain, id, filterIndex)); - setValueFromOpcode( - opcode, - filters[filterIndex].gainCC[opcode.parameters.back()], - Default::filterGainModRange - ); } break; case hash("fil&_type"): // also fil_type, filtype @@ -700,7 +685,6 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; processGenericCc(opcode, Default::eqBandwidthModRange, ModKey::createNXYZ(ModId::EqBandwidth, id, eqIndex)); - setValueFromOpcode(opcode, equalizers[eqIndex].bandwidthCC[opcode.parameters.back()], Default::eqBandwidthModRange); } break; case hash("eq&_freq"): @@ -718,7 +702,6 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; processGenericCc(opcode, Default::eqFrequencyModRange, ModKey::createNXYZ(ModId::EqFrequency, id, eqIndex)); - setValueFromOpcode(opcode, equalizers[eqIndex].frequencyCC[opcode.parameters.back()], Default::eqFrequencyModRange); } break; case hash("eq&_vel&freq"): @@ -747,7 +730,6 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; processGenericCc(opcode, Default::eqGainModRange, ModKey::createNXYZ(ModId::EqGain, id, eqIndex)); - setValueFromOpcode(opcode, equalizers[eqIndex].gainCC[opcode.parameters.back()], Default::eqGainModRange); } break; case hash("eq&_vel&gain"): diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 2b14e191..870595e8 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -99,3 +99,21 @@ width_oncc425=29 R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {0}")", })); } + +TEST_CASE("[Modulations] Filter CC connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine + cutoff=100 fil1_gain_oncc3=5 fil1_gain_stepcc3=0.5 + cutoff2=300 cutoff2_cc2=100 cutoff2_curvecc2=2 + resonance2=-1 resonance2_oncc1=2 resonance2_smoothcc1=10 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createReferenceGraph({ + R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilResonance")", + R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilCutoff")", + R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilGain")", + })); +} diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 78b5ce97..c7896667 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1313,9 +1313,6 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.filters[0].gain == 0); REQUIRE(region.filters[0].veltrack == 0); REQUIRE(region.filters[0].resonance == 0.0f); - REQUIRE(region.filters[0].cutoffCC.empty()); - REQUIRE(region.filters[0].gainCC.empty()); - REQUIRE(region.filters[0].resonanceCC.empty()); region.parseOpcode({ "cutoff2", "5000" }); REQUIRE(region.filters.size() == 2); @@ -1327,9 +1324,6 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.filters[1].gain == 0); REQUIRE(region.filters[1].veltrack == 0); REQUIRE(region.filters[1].resonance == 0.0f); - REQUIRE(region.filters[1].cutoffCC.empty()); - REQUIRE(region.filters[1].gainCC.empty()); - REQUIRE(region.filters[1].resonanceCC.empty()); region.parseOpcode({ "cutoff4", "50" }); REQUIRE(region.filters.size() == 4); @@ -1342,18 +1336,12 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.filters[2].gain == 0); REQUIRE(region.filters[2].veltrack == 0); REQUIRE(region.filters[2].resonance == 0.0f); - REQUIRE(region.filters[2].cutoffCC.empty()); - REQUIRE(region.filters[2].gainCC.empty()); - REQUIRE(region.filters[2].resonanceCC.empty()); REQUIRE(region.filters[3].keycenter == 60); REQUIRE(region.filters[3].type == FilterType::kFilterLpf2p); REQUIRE(region.filters[3].keytrack == 0); REQUIRE(region.filters[3].gain == 0); REQUIRE(region.filters[3].veltrack == 0); REQUIRE(region.filters[3].resonance == 0.0f); - REQUIRE(region.filters[3].cutoffCC.empty()); - REQUIRE(region.filters[3].gainCC.empty()); - REQUIRE(region.filters[3].resonanceCC.empty()); } SECTION("Filter parameter dispatch") @@ -1373,16 +1361,6 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.filters[1].veltrack == -100); region.parseOpcode({ "fil3_keytrack", "100" }); REQUIRE(region.filters[2].keytrack == 100); - REQUIRE(region.filters[0].cutoffCC.empty()); - region.parseOpcode({ "cutoff1_cc15", "210" }); - REQUIRE(region.filters[0].cutoffCC.contains(15)); - REQUIRE(region.filters[0].cutoffCC[15] == 210); - region.parseOpcode({ "resonance3_cc24", "10" }); - REQUIRE(region.filters[2].resonanceCC.contains(24)); - REQUIRE(region.filters[2].resonanceCC[24] == 10); - region.parseOpcode({ "fil2_gain_oncc12", "-50" }); - REQUIRE(region.filters[1].gainCC.contains(12)); - REQUIRE(region.filters[1].gainCC[12] == -50.0f); } @@ -1430,16 +1408,6 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.filters[0].gain == 96.0f); region.parseOpcode({ "fil_gain", "-200" }); REQUIRE(region.filters[0].gain == -96.0f); - - region.parseOpcode({ "cutoff_cc43", "10000" }); - REQUIRE(region.filters[0].cutoffCC[43] == 9600); - region.parseOpcode({ "cutoff_cc43", "-10000" }); - REQUIRE(region.filters[0].cutoffCC[43] == -9600); - - region.parseOpcode({ "resonance_cc43", "100" }); - REQUIRE(region.filters[0].resonanceCC[43] == 96.0f); - region.parseOpcode({ "resonance_cc43", "-5" }); - REQUIRE(region.filters[0].resonanceCC[43] == 0.0f); } SECTION("Filter types") @@ -1512,9 +1480,6 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.equalizers[0].frequency == 0.0f); REQUIRE(region.equalizers[0].vel2frequency == 0); REQUIRE(region.equalizers[0].vel2gain == 0); - REQUIRE(region.equalizers[0].frequencyCC.empty()); - REQUIRE(region.equalizers[0].bandwidthCC.empty()); - REQUIRE(region.equalizers[0].gainCC.empty()); region.parseOpcode({ "eq2_gain", "-400" }); REQUIRE(region.equalizers.size() == 2); @@ -1525,9 +1490,6 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.equalizers[1].frequency == 0.0f); REQUIRE(region.equalizers[1].vel2frequency == 0); REQUIRE(region.equalizers[1].vel2gain == 0); - REQUIRE(region.equalizers[1].frequencyCC.empty()); - REQUIRE(region.equalizers[1].bandwidthCC.empty()); - REQUIRE(region.equalizers[1].gainCC.empty()); region.parseOpcode({ "eq4_gain", "500" }); REQUIRE(region.equalizers.size() == 4); @@ -1539,16 +1501,10 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.equalizers[2].frequency == 0.0f); REQUIRE(region.equalizers[2].vel2frequency == 0); REQUIRE(region.equalizers[2].vel2gain == 0); - REQUIRE(region.equalizers[2].frequencyCC.empty()); - REQUIRE(region.equalizers[2].bandwidthCC.empty()); - REQUIRE(region.equalizers[2].gainCC.empty()); REQUIRE(region.equalizers[3].bandwidth == 1.0f); REQUIRE(region.equalizers[3].frequency == 0.0f); REQUIRE(region.equalizers[3].vel2frequency == 0); REQUIRE(region.equalizers[3].vel2gain == 0); - REQUIRE(region.equalizers[3].frequencyCC.empty()); - REQUIRE(region.equalizers[3].bandwidthCC.empty()); - REQUIRE(region.equalizers[3].gainCC.empty()); } SECTION("EQ types") @@ -1578,24 +1534,8 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.equalizers[2].vel2gain == 10.0f); region.parseOpcode({ "eq1_vel2freq", "100" }); REQUIRE(region.equalizers[0].vel2frequency == 100.0f); - REQUIRE(region.equalizers[0].bandwidthCC.empty()); - region.parseOpcode({ "eq1_bwcc24", "0.5" }); - REQUIRE(region.equalizers[0].bandwidthCC.contains(24)); - REQUIRE(region.equalizers[0].bandwidthCC[24] == 0.5f); - region.parseOpcode({ "eq1_bw_oncc24", "1.5" }); - REQUIRE(region.equalizers[0].bandwidthCC[24] == 1.5f); - region.parseOpcode({ "eq3_freqcc15", "10" }); - REQUIRE(region.equalizers[2].frequencyCC.contains(15)); - REQUIRE(region.equalizers[2].frequencyCC[15] == 10.0f); - region.parseOpcode({ "eq3_freq_oncc15", "20" }); - REQUIRE(region.equalizers[2].frequencyCC[15] == 20.0f); region.parseOpcode({ "eq1_type", "hshelf" }); REQUIRE(region.equalizers[0].type == EqType::kEqHighShelf); - region.parseOpcode({ "eq2_gaincc123", "2" }); - REQUIRE(region.equalizers[1].gainCC.contains(123)); - REQUIRE(region.equalizers[1].gainCC[123] == 2.0f); - region.parseOpcode({ "eq2_gain_oncc123", "-2" }); - REQUIRE(region.equalizers[1].gainCC[123] == -2.0f); } SECTION("EQ parameter values") @@ -1625,24 +1565,6 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.equalizers[0].vel2frequency == 30000.0f); region.parseOpcode({ "eq1_vel2freq", "-35000" }); REQUIRE(region.equalizers[0].vel2frequency == -30000.0f); - region.parseOpcode({ "eq1_bwcc15", "2" }); - REQUIRE(region.equalizers[0].bandwidthCC[15] == 2.0f); - region.parseOpcode({ "eq1_bwcc15", "-5" }); - REQUIRE(region.equalizers[0].bandwidthCC[15] == -4.0f); - region.parseOpcode({ "eq1_bwcc15", "5" }); - REQUIRE(region.equalizers[0].bandwidthCC[15] == 4.0f); - region.parseOpcode({ "eq1_gaincc15", "2" }); - REQUIRE(region.equalizers[0].gainCC[15] == 2.0f); - region.parseOpcode({ "eq1_gaincc15", "-500" }); - REQUIRE(region.equalizers[0].gainCC[15] == -96.0f); - region.parseOpcode({ "eq1_gaincc15", "500" }); - REQUIRE(region.equalizers[0].gainCC[15] == 96.0f); - region.parseOpcode({ "eq1_freqcc15", "200" }); - REQUIRE(region.equalizers[0].frequencyCC[15] == 200.0f); - region.parseOpcode({ "eq1_freqcc15", "-50000" }); - REQUIRE(region.equalizers[0].frequencyCC[15] == -30000.0f); - region.parseOpcode({ "eq1_freqcc15", "50000" }); - REQUIRE(region.equalizers[0].frequencyCC[15] == 30000.0f); } SECTION("Effects send") From fd8b00b9a195d18bbf9b4e38022421c069b78d5b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 16 Sep 2020 16:28:49 +0200 Subject: [PATCH 267/445] Update tests and connect all cc opcodes --- src/sfizz/Region.cpp | 12 ++++++------ src/sfizz/modulations/ModKey.cpp | 12 ++++++------ tests/ModulationsT.cpp | 26 ++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 662ad1a6..0f322512 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -579,7 +579,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, filters[filterIndex].resonance, Default::filterResonanceRange); } break; - case hash("cutoff&_oncc&"): // also cutoff_oncc&, cutoff_cc&, cutoff&_cc& + case_any_ccN("cutoff&"): // also cutoff_oncc&, cutoff_cc&, cutoff&_cc& { const auto filterIndex = opcode.parameters.front() - 1; if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) @@ -588,7 +588,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) processGenericCc(opcode, Default::filterCutoffModRange, ModKey::createNXYZ(ModId::FilCutoff, id, filterIndex)); } break; - case hash("resonance&_oncc&"): // also resonance_oncc&, resonance_cc&, resonance&_cc& + case_any_ccN("resonance&"): // also resonance_oncc&, resonance_cc&, resonance&_cc& { const auto filterIndex = opcode.parameters.front() - 1; if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) @@ -642,7 +642,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, filters[filterIndex].gain, Default::filterGainRange); } break; - case hash("fil&_gain_oncc&"): // also fil_gain_oncc& + case_any_ccN("fil&_gain"): // also fil_gain_oncc& { const auto filterIndex = opcode.parameters.front() - 1; if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) @@ -678,7 +678,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, equalizers[eqIndex].bandwidth, Default::eqBandwidthRange); } break; - case hash("eq&_bw_oncc&"): // also eq&_bwcc& + case_any_ccN("eq&_bw"): // also eq&_bwcc& { const auto eqIndex = opcode.parameters.front() - 1; if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) @@ -695,7 +695,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, equalizers[eqIndex].frequency, Default::eqFrequencyRange); } break; - case hash("eq&_freq_oncc&"): // also eq&_freqcc& + case_any_ccN("eq&_freq"): // also eq&_freqcc& { const auto eqIndex = opcode.parameters.front() - 1; if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) @@ -723,7 +723,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, equalizers[eqIndex].gain, Default::eqGainRange); } break; - case hash("eq&_gain_oncc&"): // also eq&_gaincc& + case_any_ccN("eq&_gain"): // also eq&_gaincc& { const auto eqIndex = opcode.parameters.front() - 1; if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs)) diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 8aa40a9d..35a68980 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -88,17 +88,17 @@ std::string ModKey::toString() const case ModId::Volume: return absl::StrCat("Volume {", region_.number(), "}"); case ModId::FilGain: - return absl::StrCat("FilterGain {", region_.number(), "N=", params_.N, "}"); + return absl::StrCat("FilterGain {", region_.number(), ", N=", params_.N, "}"); case ModId::FilCutoff: - return absl::StrCat("FilterCutoff {", region_.number(), "N=", params_.N, "}"); + return absl::StrCat("FilterCutoff {", region_.number(), ", N=", params_.N, "}"); case ModId::FilResonance: - return absl::StrCat("FilterResonance {", region_.number(), "N=", params_.N, "}"); + return absl::StrCat("FilterResonance {", region_.number(), ", N=", params_.N, "}"); case ModId::EqGain: - return absl::StrCat("EqGain {", region_.number(), "N=", params_.N, "}"); + return absl::StrCat("EqGain {", region_.number(), ", N=", params_.N, "}"); case ModId::EqFrequency: - return absl::StrCat("EqFrequency {", region_.number(), "N=", params_.N, "}"); + return absl::StrCat("EqFrequency {", region_.number(), ", N=", params_.N, "}"); case ModId::EqBandwidth: - return absl::StrCat("EqBandwitdth {", region_.number(), "N=", params_.N, "}"); + return absl::StrCat("EqBandwidth {", region_.number(), ", N=", params_.N, "}"); default: return {}; diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 870595e8..16968b7c 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -107,13 +107,31 @@ TEST_CASE("[Modulations] Filter CC connections") sample=*sine cutoff=100 fil1_gain_oncc3=5 fil1_gain_stepcc3=0.5 cutoff2=300 cutoff2_cc2=100 cutoff2_curvecc2=2 - resonance2=-1 resonance2_oncc1=2 resonance2_smoothcc1=10 + resonance3=-1 resonance3_oncc1=2 resonance3_smoothcc1=10 )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilResonance")", - R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilCutoff")", - R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilGain")", + R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilterResonance {0, N=2}")", + R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilterCutoff {0, N=1}")", + R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilterGain {0, N=0}")", + })); +} + +TEST_CASE("[Modulations] EQ CC connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine + eq1_gain_oncc2=5 eq1_gain_stepcc2=0.5 + eq2_freq_oncc3=300 eq2_freq_curvecc3=3 + eq3_bw_oncc1=2 eq3_bw_smoothcc1=10 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createReferenceGraph({ + R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "EqBandwidth {0, N=2}")", + R"("Controller 2 {curve=0, smooth=0, value=5, step=0.5}" -> "EqGain {0, N=0}")", + R"("Controller 3 {curve=3, smooth=0, value=300, step=0}" -> "EqFrequency {0, N=1}")", })); } From 3e0549e4c9e5ed2ef9109d03ac494b60626eeb4b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 16:36:33 +0200 Subject: [PATCH 268/445] Move the XML writer to its own file --- src/CMakeLists.txt | 1 + src/sfizz/Synth.cpp | 21 +++------------------ src/sfizz/utility/XmlHelpers.h | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 src/sfizz/utility/XmlHelpers.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4adbe3fb..7e04487e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,7 @@ set (SFIZZ_HEADERS sfizz/Debug.h sfizz/utility/NumericId.h sfizz/utility/SpinMutex.h + sfizz/utility/XmlHelpers.h sfizz/modulations/ModId.h sfizz/modulations/ModKey.h sfizz/modulations/ModKeyHash.h diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c984acae..1fe4ee9f 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -19,6 +19,7 @@ #include "modulations/sources/Controller.h" #include "modulations/sources/LFO.h" #include "modulations/sources/FlexEnvelope.h" +#include "utility/XmlHelpers.h" #include "pugixml.hpp" #include "absl/algorithm/container.h" #include "absl/memory/memory.h" @@ -1301,25 +1302,9 @@ std::string sfz::Synth::exportMidnam(absl::string_view model) const } } - /// - struct string_writer : pugi::xml_writer { - std::string result; - - string_writer() - { - result.reserve(8192); - } - - void write(const void* data, size_t size) override - { - result.append(static_cast(data), size); - } - }; - - /// - string_writer writer; + string_xml_writer writer; doc.save(writer); - return std::move(writer.result); + return std::move(writer.str()); } const sfz::Region* sfz::Synth::getRegionView(int idx) const noexcept diff --git a/src/sfizz/utility/XmlHelpers.h b/src/sfizz/utility/XmlHelpers.h new file mode 100644 index 00000000..11932299 --- /dev/null +++ b/src/sfizz/utility/XmlHelpers.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +class string_xml_writer : public pugi::xml_writer { +public: + explicit string_xml_writer(size_t capacity = 8192) + { + result_.reserve(capacity); + } + + void write(const void* data, size_t size) override + { + result_.append(static_cast(data), size); + } + + std::string& str() noexcept + { + return result_; + } + +private: + std::string result_; +}; From 70df2ddd1198652f0f3d6990b7f177fea92bf2c8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 16 Sep 2020 18:25:55 +0200 Subject: [PATCH 269/445] Update vstgui for EditorConfig --- editor/external/vstgui4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index 10417247..d412207f 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit 104172476111a255a87d9853a4b6502071130b9d +Subproject commit d412207f5b013a3f572c6b67d75b788401369496 From 83cd4b631796875cddf5d6c3a358bdf84eaeaea6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 18 Sep 2020 01:03:45 +0200 Subject: [PATCH 270/445] EG and LFO sources for the filters and EQs --- src/sfizz/OpcodeCleanup.cpp | 2039 +++++++++++++++++------------- src/sfizz/OpcodeCleanup.re | 10 +- src/sfizz/Region.cpp | 54 + src/sfizz/modulations/ModKey.cpp | 12 +- tests/ModulationsT.cpp | 108 +- 5 files changed, 1325 insertions(+), 898 deletions(-) diff --git a/src/sfizz/OpcodeCleanup.cpp b/src/sfizz/OpcodeCleanup.cpp index 863a6eb0..ce293269 100644 --- a/src/sfizz/OpcodeCleanup.cpp +++ b/src/sfizz/OpcodeCleanup.cpp @@ -1,4 +1,4 @@ -/* Generated by re2c 1.3 on Mon Jun 22 16:43:52 2020 */ +/* Generated by re2c 1.3 on Fri Sep 18 00:52:43 2020 */ #line 1 "src/sfizz/OpcodeCleanup.re" /* -*- mode: c++; -*- */ // SPDX-License-Identifier: BSD-2-Clause @@ -34,7 +34,7 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc size_t yynmatch; UNUSED(yynmatch); - const char* yyt1; const char* yyt2; const char* yyt3; + const char* yyt1; const char* yyt2; const char* yyt3; const char* yyt4; const char* yyt5; auto group = [&yypmatch](size_t i) -> absl::string_view { const char *beg = yypmatch[2 * i]; @@ -218,7 +218,7 @@ end_region_oncc: yy19: ++YYCURSOR; yy20: -#line 168 "src/sfizz/OpcodeCleanup.re" +#line 176 "src/sfizz/OpcodeCleanup.re" { goto end_region; } @@ -244,64 +244,65 @@ yy23: yy24: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'q': goto yy37; + case 'g': goto yy37; + case 'q': goto yy38; default: goto yy20; } yy25: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'i': goto yy38; + case 'i': goto yy39; default: goto yy20; } yy26: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'a': goto yy39; + case 'a': goto yy40; default: goto yy20; } yy27: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'i': goto yy40; + case 'i': goto yy41; default: goto yy20; } yy28: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'f': goto yy41; - case 'o': goto yy42; + case 'f': goto yy42; + case 'o': goto yy43; default: goto yy20; } yy29: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'f': goto yy43; - case 'n': goto yy44; + case 'f': goto yy44; + case 'n': goto yy45; default: goto yy20; } yy30: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'i': goto yy45; - case 'o': goto yy46; + case 'i': goto yy46; + case 'o': goto yy47; default: goto yy20; } yy31: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'e': goto yy47; + case 'e': goto yy48; default: goto yy20; } yy32: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'u': goto yy48; + case 'u': goto yy49; default: goto yy20; } yy33: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy49; + case 'p': goto yy50; default: goto yy34; } yy34: @@ -310,13 +311,13 @@ yy34: yy35: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy50; + case 'n': goto yy51; default: goto yy34; } yy36: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy51; + case 't': goto yy52; default: goto yy34; } yy37: @@ -331,112 +332,111 @@ yy37: case '6': case '7': case '8': - case '9': goto yy52; + case '9': goto yy53; default: goto yy34; } yy38: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy54; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy55; default: goto yy34; } yy39: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy55; + case 'l': goto yy57; default: goto yy34; } yy40: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy56; + case 'i': goto yy58; default: goto yy34; } yy41: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy57; + case 'r': goto yy59; default: goto yy34; } yy42: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy58; - case 'r': goto yy56; + case 'o': goto yy60; default: goto yy34; } yy43: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy59; + case 'o': goto yy61; + case 'r': goto yy59; default: goto yy34; } yy44: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy60; + case 'f': goto yy62; default: goto yy34; } yy45: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy61; + case '_': goto yy63; default: goto yy34; } yy46: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy62; + case 't': goto yy64; default: goto yy34; } yy47: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy63; + case 'l': goto yy65; default: goto yy34; } yy48: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy64; + case 's': goto yy66; default: goto yy34; } yy49: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy65; - case 'l': goto yy66; + case 'n': goto yy67; default: goto yy34; } yy50: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy67; + case 'e': goto yy68; + case 'l': goto yy69; default: goto yy34; } yy51: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy68; + case 'd': goto yy70; default: goto yy34; } yy52: yych = *++YYCURSOR; switch (yych) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': goto yy52; - case '_': goto yy69; + case 'o': goto yy71; default: goto yy34; } -yy54: +yy53: yych = *++YYCURSOR; switch (yych) { case '0': @@ -448,25 +448,24 @@ yy54: case '6': case '7': case '8': - case '9': - yyt1 = YYCURSOR; - goto yy70; + case '9': goto yy53; case '_': goto yy72; - case 'e': goto yy65; - case 'l': goto yy66; - case 't': goto yy73; default: goto yy34; } yy55: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy74; - default: goto yy34; - } -yy56: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy75; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy55; + case '_': goto yy73; default: goto yy34; } yy57: @@ -481,144 +480,145 @@ yy57: case '6': case '7': case '8': - case '9': goto yy76; + case '9': + yyt1 = YYCURSOR; + goto yy74; + case '_': goto yy76; + case 'e': goto yy68; + case 'l': goto yy69; + case 't': goto yy77; default: goto yy34; } yy58: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy78; + case 'n': goto yy78; default: goto yy34; } yy59: yych = *++YYCURSOR; switch (yych) { - case 'b': - yyt1 = YYCURSOR; - goto yy79; - case 'm': - yyt1 = YYCURSOR; - goto yy80; + case 'e': goto yy79; default: goto yy34; } yy60: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy81; - case 'l': goto yy82; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy80; default: goto yy34; } yy61: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy83; + case 'p': goto yy82; default: goto yy34; } yy62: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy84; + case 'b': + yyt1 = YYCURSOR; + goto yy83; + case 'm': + yyt1 = YYCURSOR; + goto yy84; default: goto yy34; } yy63: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy85; + case 'h': goto yy85; + case 'l': goto yy86; default: goto yy34; } yy64: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy86; + case 'c': goto yy87; default: goto yy34; } yy65: yych = *++YYCURSOR; switch (yych) { - case 'g': goto yy87; + case 'y': goto yy88; default: goto yy34; } yy66: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy88; + case 'o': goto yy89; default: goto yy34; } yy67: yych = *++YYCURSOR; switch (yych) { - case 'd': - yyt1 = YYCURSOR; - goto yy89; - case 'u': - yyt1 = YYCURSOR; - goto yy90; + case 'e': goto yy90; default: goto yy34; } yy68: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy91; + case 'g': goto yy91; default: goto yy34; } yy69: yych = *++YYCURSOR; switch (yych) { - case 'b': - yyt2 = YYCURSOR; - goto yy92; - case 'f': - yyt2 = YYCURSOR; - goto yy93; - case 'g': - yyt2 = YYCURSOR; - goto yy94; + case 'f': goto yy92; default: goto yy34; } yy70: yych = *++YYCURSOR; switch (yych) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': goto yy70; - case 't': goto yy95; + case 'd': + yyt1 = YYCURSOR; + goto yy93; + case 'u': + yyt1 = YYCURSOR; + goto yy94; + default: goto yy34; + } +yy71: + yych = *++YYCURSOR; + switch (yych) { + case 'f': goto yy95; default: goto yy34; } yy72: yych = *++YYCURSOR; - yyt1 = YYCURSOR; - goto yy99; + switch (yych) { + case 'c': + yyt2 = YYCURSOR; + goto yy96; + case 'r': + yyt2 = YYCURSOR; + goto yy97; + default: goto yy34; + } yy73: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy100; + case 'b': + yyt2 = YYCURSOR; + goto yy98; + case 'f': + yyt2 = YYCURSOR; + goto yy99; + case 'g': + yyt2 = YYCURSOR; + goto yy100; default: goto yy34; } yy74: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: - yyt2 = yyt3 = NULL; - goto yy101; - case '_': - yyt3 = YYCURSOR; - goto yy103; - default: goto yy34; - } -yy75: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy105; - default: goto yy34; - } -yy76: yych = *++YYCURSOR; switch (yych) { case '0': @@ -630,482 +630,872 @@ yy76: case '6': case '7': case '8': - case '9': goto yy76; - case '_': goto yy106; + case '9': goto yy74; + case 't': goto yy101; + default: goto yy34; + } +yy76: + yych = *++YYCURSOR; + yyt1 = YYCURSOR; + goto yy105; +yy77: + yych = *++YYCURSOR; + switch (yych) { + case 'y': goto yy106; default: goto yy34; } yy78: yych = *++YYCURSOR; switch (yych) { - case 'e': - yyt1 = YYCURSOR; + case 0x00: + yyt2 = yyt3 = NULL; goto yy107; - case 'm': - yyt1 = YYCURSOR; - goto yy108; - case 's': - yyt1 = YYCURSOR; + case '_': + yyt3 = YYCURSOR; goto yy109; default: goto yy34; } yy79: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy110; + case 'a': goto yy111; default: goto yy34; } yy80: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy111; - default: goto yy34; - } -yy81: - yych = *++YYCURSOR; - switch (yych) { - case 'i': goto yy112; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy80; + case '_': goto yy112; default: goto yy34; } yy82: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy112; + case 'e': + yyt1 = YYCURSOR; + goto yy113; + case 'm': + yyt1 = YYCURSOR; + goto yy114; + case 's': + yyt1 = YYCURSOR; + goto yy115; default: goto yy34; } yy83: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy49; + case 'y': goto yy116; default: goto yy34; } yy84: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy113; + case 'o': goto yy117; default: goto yy34; } yy85: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy114; + case 'i': goto yy118; default: goto yy34; } yy86: yych = *++YYCURSOR; switch (yych) { - case 0x00: - yyt2 = yyt3 = NULL; - goto yy115; - case '_': - yyt3 = YYCURSOR; - goto yy117; + case 'o': goto yy118; default: goto yy34; } yy87: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy119; + case 'h': goto yy50; default: goto yy34; } yy88: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy120; + case 'p': goto yy119; default: goto yy34; } yy89: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy121; + case 'n': goto yy120; default: goto yy34; } yy90: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy122; + case 0x00: + yyt2 = yyt3 = NULL; + goto yy121; + case '_': + yyt3 = YYCURSOR; + goto yy123; default: goto yy34; } yy91: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy123; + case '_': goto yy125; default: goto yy34; } yy92: yych = *++YYCURSOR; switch (yych) { - case 'w': goto yy124; + case 'o': goto yy126; default: goto yy34; } yy93: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy125; + case 'o': goto yy127; default: goto yy34; } yy94: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy126; + case 'p': goto yy128; default: goto yy34; } yy95: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy127; + case 'f': goto yy129; default: goto yy34; } yy96: + yych = *++YYCURSOR; + switch (yych) { + case 'u': goto yy130; + default: goto yy34; + } +yy97: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy131; + default: goto yy34; + } +yy98: + yych = *++YYCURSOR; + switch (yych) { + case 'w': goto yy132; + default: goto yy34; + } +yy99: + yych = *++YYCURSOR; + switch (yych) { + case 'r': goto yy133; + default: goto yy34; + } +yy100: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy134; + default: goto yy34; + } +yy101: + yych = *++YYCURSOR; + switch (yych) { + case 'y': goto yy135; + default: goto yy34; + } +yy102: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 4; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 150 "src/sfizz/OpcodeCleanup.re" +#line 158 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil1_", group(1)); goto end_region; } -#line 771 "src/sfizz/OpcodeCleanup.cpp" -yy98: +#line 826 "src/sfizz/OpcodeCleanup.cpp" +yy104: yych = *++YYCURSOR; -yy99: - if (yych <= 0x00) goto yy96; - goto yy98; -yy100: +yy105: + if (yych <= 0x00) goto yy102; + goto yy104; +yy106: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy128; + case 'p': goto yy136; default: goto yy34; } -yy101: +yy107: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 132 "src/sfizz/OpcodeCleanup.re" +#line 140 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("volume", group(1)); goto end_region; } -#line 795 "src/sfizz/OpcodeCleanup.cpp" -yy103: +#line 850 "src/sfizz/OpcodeCleanup.cpp" +yy109: yych = *++YYCURSOR; if (yych <= 0x00) { yyt2 = YYCURSOR; - goto yy101; + goto yy107; } - goto yy103; -yy105: - yych = *++YYCURSOR; - switch (yych) { - case 'l': goto yy129; - default: goto yy34; - } -yy106: - yych = *++YYCURSOR; - switch (yych) { - case 'o': - yyt2 = YYCURSOR; - goto yy130; - case 'r': - yyt2 = YYCURSOR; - goto yy131; - case 's': - yyt2 = YYCURSOR; - goto yy132; - case 'w': - yyt2 = YYCURSOR; - goto yy133; - default: goto yy34; - } -yy107: - yych = *++YYCURSOR; - switch (yych) { - case 'n': goto yy134; - default: goto yy34; - } -yy108: - yych = *++YYCURSOR; - switch (yych) { - case 'o': goto yy135; - default: goto yy34; - } -yy109: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy136; - default: goto yy34; - } -yy110: - yych = *++YYCURSOR; - if (yych <= 0x00) goto yy137; - goto yy34; + goto yy109; yy111: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy139; + case 'l': goto yy137; default: goto yy34; } yy112: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy140; - case 'h': goto yy141; + case 'c': + yyt2 = YYCURSOR; + goto yy138; + case 'o': + yyt2 = YYCURSOR; + goto yy139; + case 'r': + yyt2 = YYCURSOR; + goto yy140; + case 's': + yyt2 = YYCURSOR; + goto yy141; + case 'w': + yyt2 = YYCURSOR; + goto yy142; default: goto yy34; } yy113: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy142; + case 'n': goto yy143; default: goto yy34; } yy114: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy143; + case 'o': goto yy144; default: goto yy34; } yy115: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy145; + default: goto yy34; + } +yy116: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy146; + goto yy34; +yy117: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy148; + default: goto yy34; + } +yy118: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy149; + case 'h': goto yy150; + default: goto yy34; + } +yy119: + yych = *++YYCURSOR; + switch (yych) { + case 'h': goto yy151; + default: goto yy34; + } +yy120: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy152; + default: goto yy34; + } +yy121: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 136 "src/sfizz/OpcodeCleanup.re" +#line 144 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("pitch", group(1)); goto end_region; } -#line 885 "src/sfizz/OpcodeCleanup.cpp" -yy117: +#line 943 "src/sfizz/OpcodeCleanup.cpp" +yy123: yych = *++YYCURSOR; if (yych <= 0x00) { yyt2 = YYCURSOR; - goto yy115; + goto yy121; } - goto yy117; -yy119: + goto yy123; +yy125: yych = *++YYCURSOR; switch (yych) { case 'a': yyt2 = YYCURSOR; - goto yy144; + goto yy153; case 'd': yyt2 = YYCURSOR; - goto yy145; + goto yy154; case 'h': yyt2 = YYCURSOR; - goto yy146; + goto yy155; case 'r': yyt2 = YYCURSOR; - goto yy147; + goto yy156; case 's': yyt2 = YYCURSOR; - goto yy148; - default: goto yy34; - } -yy120: - yych = *++YYCURSOR; - switch (yych) { - case '_': goto yy149; - default: goto yy34; - } -yy121: - yych = *++YYCURSOR; - switch (yych) { - case 'w': goto yy150; - default: goto yy34; - } -yy122: - yych = *++YYCURSOR; - if (yych <= 0x00) goto yy151; - goto yy34; -yy123: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: - yyt2 = yyt3 = NULL; - goto yy153; - case '_': - yyt3 = YYCURSOR; - goto yy155; - default: goto yy34; - } -yy124: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy157; - default: goto yy34; - } -yy125: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy158; + goto yy157; default: goto yy34; } yy126: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy159; + case '_': goto yy158; default: goto yy34; } yy127: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy160; + case 'w': goto yy159; default: goto yy34; } yy128: yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy161; - default: goto yy34; - } + if (yych <= 0x00) goto yy160; + goto yy34; yy129: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy162; + case 0x00: + yyt2 = yyt3 = NULL; + goto yy162; + case '_': + yyt3 = YYCURSOR; + goto yy164; default: goto yy34; } yy130: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy163; + case 't': goto yy166; default: goto yy34; } yy131: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy164; + case 's': goto yy167; default: goto yy34; } yy132: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy165; + case 'c': goto yy168; default: goto yy34; } yy133: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy166; + case 'e': goto yy169; default: goto yy34; } yy134: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy167; + case 'i': goto yy170; default: goto yy34; } yy135: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy168; + case 'p': goto yy171; default: goto yy34; } yy136: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy169; + case 'e': goto yy172; default: goto yy34; } yy137: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy173; + default: goto yy34; + } +yy138: + yych = *++YYCURSOR; + switch (yych) { + case 'u': goto yy174; + default: goto yy34; + } +yy139: + yych = *++YYCURSOR; + switch (yych) { + case 'f': goto yy175; + default: goto yy34; + } +yy140: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy176; + case 'e': goto yy177; + default: goto yy34; + } +yy141: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy178; + default: goto yy34; + } +yy142: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy179; + default: goto yy34; + } +yy143: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy180; + default: goto yy34; + } +yy144: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy181; + default: goto yy34; + } +yy145: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy182; + default: goto yy34; + } +yy146: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 3; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 110 "src/sfizz/OpcodeCleanup.re" +#line 118 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("off_", group(1)); goto end_region; } -#line 1030 "src/sfizz/OpcodeCleanup.cpp" -yy139: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy110; - default: goto yy34; - } -yy140: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy170; - default: goto yy34; - } -yy141: - yych = *++YYCURSOR; - switch (yych) { - case 'd': goto yy171; - default: goto yy34; - } -yy142: - yych = *++YYCURSOR; - switch (yych) { - case 'o': goto yy172; - default: goto yy34; - } -yy143: - yych = *++YYCURSOR; - switch (yych) { - case 'n': goto yy173; - default: goto yy34; - } -yy144: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy174; - default: goto yy34; - } -yy145: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy175; - default: goto yy34; - } -yy146: - yych = *++YYCURSOR; - switch (yych) { - case 'o': goto yy176; - default: goto yy34; - } -yy147: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy177; - default: goto yy34; - } +#line 1107 "src/sfizz/OpcodeCleanup.cpp" yy148: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy178; - case 'u': goto yy179; + case 'e': goto yy116; default: goto yy34; } yy149: yych = *++YYCURSOR; switch (yych) { - case 'd': - yyt2 = YYCURSOR; - goto yy180; - case 'f': - yyt2 = YYCURSOR; - goto yy181; + case 'c': goto yy183; default: goto yy34; } yy150: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy122; + case 'd': goto yy184; default: goto yy34; } yy151: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy185; + default: goto yy34; + } +yy152: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy186; + default: goto yy34; + } +yy153: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy187; + default: goto yy34; + } +yy154: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy188; + default: goto yy34; + } +yy155: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy189; + default: goto yy34; + } +yy156: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy190; + default: goto yy34; + } +yy157: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy191; + case 'u': goto yy192; + default: goto yy34; + } +yy158: + yych = *++YYCURSOR; + switch (yych) { + case 'd': + yyt2 = YYCURSOR; + goto yy193; + case 'f': + yyt2 = YYCURSOR; + goto yy194; + default: goto yy34; + } +yy159: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy128; + default: goto yy34; + } +yy160: + ++YYCURSOR; + yynmatch = 2; + yypmatch[2] = yyt1; + yypmatch[0] = yyt1 - 4; + yypmatch[1] = YYCURSOR; + yypmatch[3] = YYCURSOR - 1; +#line 122 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat("bend_", group(1)); + goto end_region; + } +#line 1198 "src/sfizz/OpcodeCleanup.cpp" +yy162: + ++YYCURSOR; + yynmatch = 2; + yypmatch[0] = yyt1; + yypmatch[2] = yyt3; + yypmatch[3] = yyt2; + yypmatch[1] = YYCURSOR; +#line 162 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat("cutoff1", group(1)); + goto end_region; + } +#line 1211 "src/sfizz/OpcodeCleanup.cpp" +yy164: + yych = *++YYCURSOR; + if (yych <= 0x00) { + yyt2 = YYCURSOR; + goto yy162; + } + goto yy164; +yy166: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy195; + default: goto yy34; + } +yy167: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy196; + default: goto yy34; + } +yy168: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy197; + default: goto yy34; + } +yy169: + yych = *++YYCURSOR; + switch (yych) { + case 'q': goto yy132; + default: goto yy34; + } +yy170: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy132; + default: goto yy34; + } +yy171: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy198; + default: goto yy34; + } +yy172: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy199; + goto yy34; +yy173: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy201; + default: goto yy34; + } +yy174: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy202; + default: goto yy34; + } +yy175: + yych = *++YYCURSOR; + switch (yych) { + case 'f': goto yy203; + default: goto yy34; + } +yy176: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy204; + default: goto yy34; + } +yy177: + yych = *++YYCURSOR; + switch (yych) { + case 's': goto yy205; + default: goto yy34; + } +yy178: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy206; + default: goto yy34; + } +yy179: + yych = *++YYCURSOR; + switch (yych) { + case 'v': goto yy207; + default: goto yy34; + } +yy180: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy208; + goto yy34; +yy181: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy180; + default: goto yy34; + } +yy182: + yych = *++YYCURSOR; + switch (yych) { + case 'r': goto yy210; + default: goto yy34; + } +yy183: + yych = *++YYCURSOR; + switch (yych) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + yyt1 = YYCURSOR; + goto yy211; + default: goto yy34; + } +yy184: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy213; + default: goto yy34; + } +yy185: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy214; + default: goto yy34; + } +yy186: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy215; + default: goto yy34; + } +yy187: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy216; + default: goto yy34; + } +yy188: + yych = *++YYCURSOR; + switch (yych) { + case 'c': + case 'l': goto yy217; + default: goto yy34; + } +yy189: + yych = *++YYCURSOR; + switch (yych) { + case 'l': goto yy218; + default: goto yy34; + } +yy190: + yych = *++YYCURSOR; + switch (yych) { + case 'l': goto yy219; + default: goto yy34; + } +yy191: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy220; + default: goto yy34; + } +yy192: + yych = *++YYCURSOR; + switch (yych) { + case 's': goto yy221; + default: goto yy34; + } +yy193: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy222; + default: goto yy34; + } +yy194: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy223; + case 'r': goto yy224; + default: goto yy34; + } +yy195: + yych = *++YYCURSOR; + switch (yych) { + case 'f': goto yy225; + default: goto yy34; + } +yy196: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy226; + default: goto yy34; + } +yy197: + yych = *++YYCURSOR; + switch (yych) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + yyt3 = YYCURSOR; + goto yy227; + default: goto yy34; + } +yy198: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy229; + goto yy34; +yy199: + ++YYCURSOR; + yynmatch = 1; + yypmatch[0] = YYCURSOR - 8; + yypmatch[1] = YYCURSOR; +#line 126 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat("fil1_type"); + goto end_region; + } +#line 1445 "src/sfizz/OpcodeCleanup.cpp" +yy201: + yych = *++YYCURSOR; + switch (yych) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + yyt1 = YYCURSOR; + goto yy231; + default: goto yy34; + } +yy202: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy233; + default: goto yy34; + } +yy203: + yych = *++YYCURSOR; + switch (yych) { + case 's': goto yy234; + default: goto yy34; + } +yy204: + yych = *++YYCURSOR; + switch (yych) { + case 'i': goto yy235; + default: goto yy34; + } +yy205: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy236; + default: goto yy34; + } +yy206: + yych = *++YYCURSOR; + switch (yych) { + case 'l': goto yy207; + default: goto yy34; + } +yy207: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy237; + default: goto yy34; + } +yy208: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; @@ -1113,403 +1503,150 @@ yy151: yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; #line 114 "src/sfizz/OpcodeCleanup.re" - { - opcode = absl::StrCat("bend_", group(1)); - goto end_region; - } -#line 1121 "src/sfizz/OpcodeCleanup.cpp" -yy153: - ++YYCURSOR; - yynmatch = 2; - yypmatch[0] = yyt1; - yypmatch[2] = yyt3; - yypmatch[3] = yyt2; - yypmatch[1] = YYCURSOR; -#line 154 "src/sfizz/OpcodeCleanup.re" - { - opcode = absl::StrCat("cutoff1", group(1)); - goto end_region; - } -#line 1134 "src/sfizz/OpcodeCleanup.cpp" -yy155: - yych = *++YYCURSOR; - if (yych <= 0x00) { - yyt2 = YYCURSOR; - goto yy153; - } - goto yy155; -yy157: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy182; - default: goto yy34; - } -yy158: - yych = *++YYCURSOR; - switch (yych) { - case 'q': goto yy124; - default: goto yy34; - } -yy159: - yych = *++YYCURSOR; - switch (yych) { - case 'n': goto yy124; - default: goto yy34; - } -yy160: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy183; - default: goto yy34; - } -yy161: - yych = *++YYCURSOR; - if (yych <= 0x00) goto yy184; - goto yy34; -yy162: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy186; - default: goto yy34; - } -yy163: - yych = *++YYCURSOR; - switch (yych) { - case 'f': goto yy187; - default: goto yy34; - } -yy164: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy188; - default: goto yy34; - } -yy165: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy189; - default: goto yy34; - } -yy166: - yych = *++YYCURSOR; - switch (yych) { - case 'v': goto yy190; - default: goto yy34; - } -yy167: - yych = *++YYCURSOR; - if (yych <= 0x00) goto yy191; - goto yy34; -yy168: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy167; - default: goto yy34; - } -yy169: - yych = *++YYCURSOR; - switch (yych) { - case 'r': goto yy193; - default: goto yy34; - } -yy170: - yych = *++YYCURSOR; - switch (yych) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - yyt1 = YYCURSOR; - goto yy194; - default: goto yy34; - } -yy171: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy196; - default: goto yy34; - } -yy172: - yych = *++YYCURSOR; - switch (yych) { - case 'n': goto yy197; - default: goto yy34; - } -yy173: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy198; - default: goto yy34; - } -yy174: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy199; - default: goto yy34; - } -yy175: - yych = *++YYCURSOR; - switch (yych) { - case 'c': - case 'l': goto yy200; - default: goto yy34; - } -yy176: - yych = *++YYCURSOR; - switch (yych) { - case 'l': goto yy201; - default: goto yy34; - } -yy177: - yych = *++YYCURSOR; - switch (yych) { - case 'l': goto yy202; - default: goto yy34; - } -yy178: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy203; - default: goto yy34; - } -yy179: - yych = *++YYCURSOR; - switch (yych) { - case 's': goto yy204; - default: goto yy34; - } -yy180: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy205; - default: goto yy34; - } -yy181: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy206; - case 'r': goto yy207; - default: goto yy34; - } -yy182: - yych = *++YYCURSOR; - switch (yych) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - yyt3 = YYCURSOR; - goto yy208; - default: goto yy34; - } -yy183: - yych = *++YYCURSOR; - if (yych <= 0x00) goto yy210; - goto yy34; -yy184: - ++YYCURSOR; - yynmatch = 1; - yypmatch[0] = YYCURSOR - 8; - yypmatch[1] = YYCURSOR; -#line 118 "src/sfizz/OpcodeCleanup.re" - { - opcode = absl::StrCat("fil1_type"); - goto end_region; - } -#line 1332 "src/sfizz/OpcodeCleanup.cpp" -yy186: - yych = *++YYCURSOR; - switch (yych) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - yyt1 = YYCURSOR; - goto yy212; - default: goto yy34; - } -yy187: - yych = *++YYCURSOR; - switch (yych) { - case 's': goto yy214; - default: goto yy34; - } -yy188: - yych = *++YYCURSOR; - switch (yych) { - case 'i': goto yy215; - default: goto yy34; - } -yy189: - yych = *++YYCURSOR; - switch (yych) { - case 'l': goto yy190; - default: goto yy34; - } -yy190: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy216; - default: goto yy34; - } -yy191: - ++YYCURSOR; - yynmatch = 2; - yypmatch[2] = yyt1; - yypmatch[0] = yyt1 - 4; - yypmatch[1] = YYCURSOR; - yypmatch[3] = YYCURSOR - 1; -#line 106 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("loop_", group(1)); goto end_region; } -#line 1386 "src/sfizz/OpcodeCleanup.cpp" -yy193: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy167; - default: goto yy34; - } -yy194: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: goto yy217; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': goto yy194; - default: goto yy34; - } -yy196: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy219; - default: goto yy34; - } -yy197: - yych = *++YYCURSOR; - switch (yych) { - case 'y': goto yy220; - default: goto yy34; - } -yy198: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy221; - default: goto yy34; - } -yy199: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy222; - default: goto yy34; - } -yy200: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy223; - default: goto yy34; - } -yy201: - yych = *++YYCURSOR; - switch (yych) { - case 'd': goto yy224; - default: goto yy34; - } -yy202: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy225; - default: goto yy34; - } -yy203: - yych = *++YYCURSOR; - switch (yych) { - case 'r': goto yy226; - default: goto yy34; - } -yy204: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy227; - default: goto yy34; - } -yy205: - yych = *++YYCURSOR; - switch (yych) { - case 'p': goto yy228; - default: goto yy34; - } -yy206: - yych = *++YYCURSOR; - switch (yych) { - case 'd': goto yy229; - default: goto yy34; - } -yy207: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy230; - default: goto yy34; - } -yy208: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: goto yy231; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': goto yy208; - default: goto yy34; - } +#line 1511 "src/sfizz/OpcodeCleanup.cpp" yy210: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy180; + default: goto yy34; + } +yy211: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: goto yy238; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy211; + default: goto yy34; + } +yy213: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy240; + default: goto yy34; + } +yy214: + yych = *++YYCURSOR; + switch (yych) { + case 'y': goto yy241; + default: goto yy34; + } +yy215: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy242; + default: goto yy34; + } +yy216: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy243; + default: goto yy34; + } +yy217: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy244; + default: goto yy34; + } +yy218: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy245; + default: goto yy34; + } +yy219: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy246; + default: goto yy34; + } +yy220: + yych = *++YYCURSOR; + switch (yych) { + case 'r': goto yy247; + default: goto yy34; + } +yy221: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy248; + default: goto yy34; + } +yy222: + yych = *++YYCURSOR; + switch (yych) { + case 'p': goto yy249; + default: goto yy34; + } +yy223: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy250; + default: goto yy34; + } +yy224: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy251; + default: goto yy34; + } +yy225: + yych = *++YYCURSOR; + switch (yych) { + case 'f': goto yy252; + default: goto yy34; + } +yy226: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy253; + default: goto yy34; + } +yy227: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: goto yy254; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy227; + default: goto yy34; + } +yy229: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 3; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 5; -#line 122 "src/sfizz/OpcodeCleanup.re" +#line 130 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil", group(1), "_type"); goto end_region; } -#line 1509 "src/sfizz/OpcodeCleanup.cpp" -yy212: +#line 1646 "src/sfizz/OpcodeCleanup.cpp" +yy231: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy233; + case 0x00: goto yy256; case '0': case '1': case '2': @@ -1519,26 +1656,38 @@ yy212: case '6': case '7': case '8': - case '9': goto yy212; + case '9': goto yy231; default: goto yy34; } -yy214: +yy233: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy235; + case 'f': goto yy258; default: goto yy34; } -yy215: +yy234: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy216; + case 'e': goto yy259; default: goto yy34; } -yy216: +yy235: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy236; + switch (yych) { + case 'o': goto yy237; + default: goto yy34; + } +yy236: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy260; + default: goto yy34; + } +yy237: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy261; goto yy34; -yy217: +yy238: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1547,13 +1696,13 @@ yy217: yypmatch[2] = yyt1 - 4; yypmatch[3] = yyt1 - 2; yypmatch[5] = YYCURSOR - 1; -#line 141 "src/sfizz/OpcodeCleanup.re" +#line 149 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("start_", group(1), "cc", group(2)); goto end_region; } -#line 1556 "src/sfizz/OpcodeCleanup.cpp" -yy219: +#line 1705 "src/sfizz/OpcodeCleanup.cpp" +yy240: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1567,81 +1716,99 @@ yy219: case '8': case '9': yyt1 = YYCURSOR; - goto yy238; + goto yy263; default: goto yy34; } -yy220: +yy241: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy240; + case '_': goto yy265; default: goto yy34; } -yy221: +yy242: yych = *++YYCURSOR; switch (yych) { case 0x00: yyt2 = yyt3 = NULL; - goto yy241; + goto yy266; case '_': yyt3 = YYCURSOR; - goto yy243; + goto yy268; default: goto yy34; } -yy222: +yy243: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy245; + case 'c': goto yy270; default: goto yy34; } -yy223: +yy244: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy224; + case 'y': goto yy245; default: goto yy34; } -yy224: +yy245: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy246; + case 'c': goto yy271; default: goto yy34; } -yy225: +yy246: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy247; + case 'a': goto yy272; default: goto yy34; } -yy226: +yy247: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy224; + case 't': goto yy245; default: goto yy34; } -yy227: +yy248: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy248; + case 'a': goto yy273; default: goto yy34; } -yy228: +yy249: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy249; + case 't': goto yy274; default: goto yy34; } -yy229: +yy250: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy250; + case 'e': goto yy275; default: goto yy34; } -yy230: +yy251: yych = *++YYCURSOR; switch (yych) { - case 'q': goto yy250; + case 'q': goto yy275; default: goto yy34; } -yy231: +yy252: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: + yyt4 = yyt5 = NULL; + yyt3 = YYCURSOR; + goto yy276; + case '_': + yyt3 = yyt5 = YYCURSOR; + goto yy278; + default: goto yy34; + } +yy253: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy280; + default: goto yy34; + } +yy254: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -1652,13 +1819,13 @@ yy231: yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 97 "src/sfizz/OpcodeCleanup.re" +#line 98 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3)); goto end_region; } -#line 1661 "src/sfizz/OpcodeCleanup.cpp" -yy233: +#line 1828 "src/sfizz/OpcodeCleanup.cpp" +yy256: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1667,19 +1834,31 @@ yy233: yypmatch[2] = yyt1 - 8; yypmatch[3] = yyt1 - 6; yypmatch[5] = YYCURSOR - 1; -#line 163 "src/sfizz/OpcodeCleanup.re" +#line 171 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "hdcc", group(2)); goto end_region; } -#line 1676 "src/sfizz/OpcodeCleanup.cpp" -yy235: +#line 1843 "src/sfizz/OpcodeCleanup.cpp" +yy258: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy216; + case 'f': goto yy281; default: goto yy34; } -yy236: +yy259: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy237; + default: goto yy34; + } +yy260: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy282; + default: goto yy34; + } +yy261: ++YYCURSOR; yynmatch = 3; yypmatch[2] = yyt1; @@ -1688,16 +1867,16 @@ yy236: yypmatch[1] = YYCURSOR; yypmatch[3] = yyt2 - 1; yypmatch[5] = YYCURSOR - 1; -#line 101 "src/sfizz/OpcodeCleanup.re" +#line 102 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "1"); goto end_region; } -#line 1697 "src/sfizz/OpcodeCleanup.cpp" -yy238: +#line 1876 "src/sfizz/OpcodeCleanup.cpp" +yy263: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy251; + case 0x00: goto yy283; case '0': case '1': case '2': @@ -1707,72 +1886,120 @@ yy238: case '6': case '7': case '8': - case '9': goto yy238; + case '9': goto yy263; default: goto yy34; } -yy240: +yy265: yych = *++YYCURSOR; switch (yych) { - case 'g': goto yy253; + case 'g': goto yy285; default: goto yy34; } -yy241: +yy266: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 158 "src/sfizz/OpcodeCleanup.re" +#line 166 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("resonance1", group(1)); goto end_region; } -#line 1732 "src/sfizz/OpcodeCleanup.cpp" -yy243: +#line 1911 "src/sfizz/OpcodeCleanup.cpp" +yy268: yych = *++YYCURSOR; if (yych <= 0x00) { yyt2 = YYCURSOR; - goto yy241; + goto yy266; } - goto yy243; -yy245: + goto yy268; +yy270: yych = *++YYCURSOR; switch (yych) { - case 'k': goto yy224; + case 'k': goto yy245; default: goto yy34; } -yy246: +yy271: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy254; + case 'c': goto yy286; default: goto yy34; } -yy247: +yy272: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy255; + case 's': goto yy287; default: goto yy34; } -yy248: +yy273: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy256; + case 'i': goto yy288; default: goto yy34; } -yy249: +yy274: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy250; + case 'h': goto yy275; default: goto yy34; } -yy250: +yy275: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy257; + case 'c': goto yy289; default: goto yy34; } -yy251: +yy276: + ++YYCURSOR; + yynmatch = 4; + yypmatch[2] = yyt1; + yypmatch[4] = yyt2; + yypmatch[5] = yyt3; + yypmatch[6] = yyt5; + yypmatch[7] = yyt4; + yypmatch[0] = yyt1; + yypmatch[1] = YYCURSOR; + yypmatch[3] = yyt2 - 1; +#line 110 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); + goto end_region; + } +#line 1971 "src/sfizz/OpcodeCleanup.cpp" +yy278: + yych = *++YYCURSOR; + if (yych <= 0x00) { + yyt4 = YYCURSOR; + goto yy276; + } + goto yy278; +yy280: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy290; + default: goto yy34; + } +yy281: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: + yyt4 = yyt5 = NULL; + yyt3 = YYCURSOR; + goto yy291; + case '_': + yyt3 = yyt5 = YYCURSOR; + goto yy293; + default: goto yy34; + } +yy282: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy295; + default: goto yy34; + } +yy283: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1781,19 +2008,19 @@ yy251: yypmatch[2] = yyt1 - 6; yypmatch[3] = yyt1 - 4; yypmatch[5] = YYCURSOR - 1; -#line 145 "src/sfizz/OpcodeCleanup.re" +#line 153 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("start_", group(1), "hdcc", group(2)); goto end_region; } -#line 1790 "src/sfizz/OpcodeCleanup.cpp" -yy253: +#line 2017 "src/sfizz/OpcodeCleanup.cpp" +yy285: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy258; + case 'r': goto yy296; default: goto yy34; } -yy254: +yy286: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1807,37 +2034,73 @@ yy254: case '8': case '9': yyt3 = YYCURSOR; - goto yy259; + goto yy297; default: goto yy34; } -yy255: +yy287: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy224; + case 'e': goto yy245; default: goto yy34; } -yy256: +yy288: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy224; + case 'n': goto yy245; default: goto yy34; } -yy257: +yy289: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy261; + case 'c': goto yy299; default: goto yy34; } -yy258: +yy290: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy262; + case 'e': goto yy252; default: goto yy34; } -yy259: +yy291: + ++YYCURSOR; + yynmatch = 4; + yypmatch[2] = yyt1; + yypmatch[4] = yyt2; + yypmatch[5] = yyt3; + yypmatch[6] = yyt5; + yypmatch[7] = yyt4; + yypmatch[0] = yyt1; + yypmatch[1] = YYCURSOR; + yypmatch[3] = yyt2 - 1; +#line 106 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); + goto end_region; + } +#line 2081 "src/sfizz/OpcodeCleanup.cpp" +yy293: + yych = *++YYCURSOR; + if (yych <= 0x00) { + yyt4 = YYCURSOR; + goto yy291; + } + goto yy293; +yy295: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy263; + case 'c': goto yy300; + default: goto yy34; + } +yy296: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy301; + default: goto yy34; + } +yy297: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: goto yy302; case '0': case '1': case '2': @@ -1847,10 +2110,10 @@ yy259: case '6': case '7': case '8': - case '9': goto yy259; + case '9': goto yy297; default: goto yy34; } -yy261: +yy299: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1864,16 +2127,22 @@ yy261: case '8': case '9': yyt3 = YYCURSOR; - goto yy265; + goto yy304; default: goto yy34; } -yy262: +yy300: yych = *++YYCURSOR; switch (yych) { - case 'u': goto yy267; + case 'e': goto yy281; default: goto yy34; } -yy263: +yy301: + yych = *++YYCURSOR; + switch (yych) { + case 'u': goto yy306; + default: goto yy34; + } +yy302: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -1884,16 +2153,16 @@ yy263: yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 93 "src/sfizz/OpcodeCleanup.re" +#line 94 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3)); goto end_region; } -#line 1893 "src/sfizz/OpcodeCleanup.cpp" -yy265: +#line 2162 "src/sfizz/OpcodeCleanup.cpp" +yy304: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy268; + case 0x00: goto yy307; case '0': case '1': case '2': @@ -1903,16 +2172,16 @@ yy265: case '6': case '7': case '8': - case '9': goto yy265; + case '9': goto yy304; default: goto yy34; } -yy267: +yy306: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy270; + case 'p': goto yy309; default: goto yy34; } -yy268: +yy307: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -1923,27 +2192,27 @@ yy268: yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 89 "src/sfizz/OpcodeCleanup.re" +#line 90 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3)); goto end_region; } -#line 1932 "src/sfizz/OpcodeCleanup.cpp" -yy270: +#line 2201 "src/sfizz/OpcodeCleanup.cpp" +yy309: yych = *++YYCURSOR; if (yych >= 0x01) goto yy34; ++YYCURSOR; yynmatch = 1; yypmatch[0] = YYCURSOR - 16; yypmatch[1] = YYCURSOR; -#line 127 "src/sfizz/OpcodeCleanup.re" +#line 135 "src/sfizz/OpcodeCleanup.re" { opcode = "group"; goto end_region; } -#line 1945 "src/sfizz/OpcodeCleanup.cpp" +#line 2214 "src/sfizz/OpcodeCleanup.cpp" } -#line 172 "src/sfizz/OpcodeCleanup.re" +#line 180 "src/sfizz/OpcodeCleanup.re" end_region: @@ -1958,80 +2227,80 @@ end_region: YYCURSOR = opcode.c_str(); -#line 1962 "src/sfizz/OpcodeCleanup.cpp" +#line 2231 "src/sfizz/OpcodeCleanup.cpp" { char yych; yych = *YYCURSOR; switch (yych) { - case 's': goto yy277; - default: goto yy275; + case 's': goto yy316; + default: goto yy314; } -yy275: +yy314: ++YYCURSOR; -yy276: -#line 192 "src/sfizz/OpcodeCleanup.re" +yy315: +#line 200 "src/sfizz/OpcodeCleanup.re" { goto end_control; } -#line 1977 "src/sfizz/OpcodeCleanup.cpp" -yy277: +#line 2246 "src/sfizz/OpcodeCleanup.cpp" +yy316: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'e': goto yy278; - default: goto yy276; + case 'e': goto yy317; + default: goto yy315; } -yy278: +yy317: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy280; - default: goto yy279; + case 't': goto yy319; + default: goto yy318; } -yy279: +yy318: YYCURSOR = YYMARKER; - goto yy276; -yy280: + goto yy315; +yy319: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy281; - default: goto yy279; + case '_': goto yy320; + default: goto yy318; } -yy281: +yy320: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy282; - default: goto yy279; + case 'r': goto yy321; + default: goto yy318; } -yy282: +yy321: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy283; - default: goto yy279; + case 'e': goto yy322; + default: goto yy318; } -yy283: +yy322: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy284; - default: goto yy279; + case 'a': goto yy323; + default: goto yy318; } -yy284: +yy323: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy285; - default: goto yy279; + case 'l': goto yy324; + default: goto yy318; } -yy285: +yy324: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy286; - default: goto yy279; + case 'c': goto yy325; + default: goto yy318; } -yy286: +yy325: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy287; - default: goto yy279; + case 'c': goto yy326; + default: goto yy318; } -yy287: +yy326: yych = *++YYCURSOR; switch (yych) { case '0': @@ -2045,13 +2314,13 @@ yy287: case '8': case '9': yyt1 = YYCURSOR; - goto yy288; - default: goto yy279; + goto yy327; + default: goto yy318; } -yy288: +yy327: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy290; + case 0x00: goto yy329; case '0': case '1': case '2': @@ -2061,24 +2330,24 @@ yy288: case '6': case '7': case '8': - case '9': goto yy288; - default: goto yy279; + case '9': goto yy327; + default: goto yy318; } -yy290: +yy329: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 10; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 187 "src/sfizz/OpcodeCleanup.re" +#line 195 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("set_hdcc", group(1)); goto end_control; } -#line 2080 "src/sfizz/OpcodeCleanup.cpp" +#line 2349 "src/sfizz/OpcodeCleanup.cpp" } -#line 196 "src/sfizz/OpcodeCleanup.re" +#line 204 "src/sfizz/OpcodeCleanup.re" end_control: diff --git a/src/sfizz/OpcodeCleanup.re b/src/sfizz/OpcodeCleanup.re index 3b0774e2..c744d4ed 100644 --- a/src/sfizz/OpcodeCleanup.re +++ b/src/sfizz/OpcodeCleanup.re @@ -85,6 +85,7 @@ end_region_oncc: egV1 = "ampeg"|"fileg"|"pitcheg"; eqV1 = "eq" number; lfoV2 = "lfo" number; + egV2 = "eg" number; (lfoV1) "_" ("depth"|"freq"|"fade") "cc" (number) END { opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3)); @@ -102,7 +103,14 @@ end_region_oncc: opcode = absl::StrCat(group(1), "_", group(2), "1"); goto end_region; } - + (lfoV2) "_" ("cutoff"|"resonance") ("_" any)? END { + opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); + goto end_region; + } + (egV2) "_" ("cutoff"|"resonance") ("_" any)? END { + opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); + goto end_region; + } "loop" ("mode"|"start"|"end") END { opcode = absl::StrCat("loop_", group(1)); goto end_region; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 0f322512..f2097395 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -45,6 +45,23 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash(x "_stepcc&"): \ case hash(x "_smoothcc&") + #define LFO_EG_filter_EQ_target(sourceKey, targetKey, range) \ + { \ + const auto number = opcode.parameters.front(); \ + if (number == 0) \ + return false; \ + \ + const auto index = opcode.parameters.size() == 2 ? opcode.parameters.back() - 1 : 0; \ + if (!extendIfNecessary(filters, index + 1, Default::numFilters)) \ + return false; \ + \ + if (auto value = readOpcode(opcode.value, range)) { \ + const ModKey source = ModKey::createNXYZ(sourceKey, id, number - 1); \ + const ModKey target = ModKey::createNXYZ(targetKey, id, index); \ + getOrCreateConnection(source, target).sourceDepth = *value; \ + } \ + } + // Sound source: sample playback case hash("sample"): { @@ -1016,6 +1033,24 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) } } break; + case hash("lfo&_cutoff&"): + LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilCutoff, Default::filterCutoffModRange); + break; + case hash("lfo&_resonance&"): + LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilResonance, Default::filterResonanceModRange); + break; + case hash("lfo&_fil&_gain"): + LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilGain, Default::filterGainModRange); + break; + case hash("lfo&_eq&gain"): + LFO_EG_filter_EQ_target(ModId::LFO, ModId::EqGain, Default::eqGainModRange); + break; + case hash("lfo&_eq&freq"): + LFO_EG_filter_EQ_target(ModId::LFO, ModId::EqFrequency, Default::eqFrequencyModRange); + break; + case hash("lfo&_eq&bw"): + LFO_EG_filter_EQ_target(ModId::LFO, ModId::EqBandwidth, Default::eqBandwidthModRange); + break; // Modulation: Flex EG (targets) case hash("eg&_amplitude"): @@ -1090,6 +1125,24 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) } } break; + case hash("eg&_cutoff&"): + LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilCutoff, Default::filterCutoffModRange); + break; + case hash("eg&_resonance&"): + LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilResonance, Default::filterResonanceModRange); + break; + case hash("eg&_fil&_gain"): + LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilGain, Default::filterGainModRange); + break; + case hash("eg&_eq&gain"): + LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqGain, Default::eqGainModRange); + break; + case hash("eg&_eq&freq"): + LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqFrequency, Default::eqFrequencyModRange); + break; + case hash("eg&_eq&bw"): + LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqBandwidth, Default::eqBandwidthModRange); + break; // Amplitude Envelope case hash("ampeg_attack"): @@ -1292,6 +1345,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return false; #undef case_any_ccN + #undef LFO_EG_filter_EQ_target } return true; diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 35a68980..3615c976 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -88,17 +88,17 @@ std::string ModKey::toString() const case ModId::Volume: return absl::StrCat("Volume {", region_.number(), "}"); case ModId::FilGain: - return absl::StrCat("FilterGain {", region_.number(), ", N=", params_.N, "}"); + return absl::StrCat("FilterGain {", region_.number(), ", N=", 1 + params_.N, "}"); case ModId::FilCutoff: - return absl::StrCat("FilterCutoff {", region_.number(), ", N=", params_.N, "}"); + return absl::StrCat("FilterCutoff {", region_.number(), ", N=", 1 + params_.N, "}"); case ModId::FilResonance: - return absl::StrCat("FilterResonance {", region_.number(), ", N=", params_.N, "}"); + return absl::StrCat("FilterResonance {", region_.number(), ", N=", 1 + params_.N, "}"); case ModId::EqGain: - return absl::StrCat("EqGain {", region_.number(), ", N=", params_.N, "}"); + return absl::StrCat("EqGain {", region_.number(), ", N=", 1 + params_.N, "}"); case ModId::EqFrequency: - return absl::StrCat("EqFrequency {", region_.number(), ", N=", params_.N, "}"); + return absl::StrCat("EqFrequency {", region_.number(), ", N=", 1 + params_.N, "}"); case ModId::EqBandwidth: - return absl::StrCat("EqBandwidth {", region_.number(), ", N=", params_.N, "}"); + return absl::StrCat("EqBandwidth {", region_.number(), ", N=", 1 + params_.N, "}"); default: return {}; diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 16968b7c..45db5737 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -112,9 +112,9 @@ TEST_CASE("[Modulations] Filter CC connections") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilterResonance {0, N=2}")", - R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilterCutoff {0, N=1}")", - R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilterGain {0, N=0}")", + R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilterResonance {0, N=3}")", + R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilterCutoff {0, N=2}")", + R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilterGain {0, N=1}")", })); } @@ -130,8 +130,104 @@ TEST_CASE("[Modulations] EQ CC connections") const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "EqBandwidth {0, N=2}")", - R"("Controller 2 {curve=0, smooth=0, value=5, step=0.5}" -> "EqGain {0, N=0}")", - R"("Controller 3 {curve=3, smooth=0, value=300, step=0}" -> "EqFrequency {0, N=1}")", + R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "EqBandwidth {0, N=3}")", + R"("Controller 2 {curve=0, smooth=0, value=5, step=0.5}" -> "EqGain {0, N=1}")", + R"("Controller 3 {curve=3, smooth=0, value=300, step=0}" -> "EqFrequency {0, N=2}")", + })); +} + +TEST_CASE("[Modulations] LFO Filter connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine + lfo1_freq=0.1 lfo1_cutoff1=1 + lfo2_freq=1 lfo2_cutoff=2 + lfo3_freq=2 lfo3_resonance=3 + lfo4_freq=0.5 lfo4_resonance1=4 + lfo5_freq=0.5 lfo5_resonance2=5 + lfo6_freq=3 lfo6_fil1_gain=-1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createReferenceGraph({ + R"("LFO 1 {0}" -> "FilterCutoff {0, N=1}")", + R"("LFO 2 {0}" -> "FilterCutoff {0, N=1}")", + R"("LFO 3 {0}" -> "FilterResonance {0, N=1}")", + R"("LFO 4 {0}" -> "FilterResonance {0, N=1}")", + R"("LFO 5 {0}" -> "FilterResonance {0, N=2}")", + R"("LFO 6 {0}" -> "FilterGain {0, N=1}")", + })); +} + +TEST_CASE("[Modulations] EG Filter connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine + eg1_time1=0.1 eg1_cutoff1=1 + eg2_time1=1 eg2_cutoff=2 + eg3_time1=2 eg3_resonance=3 + eg4_time1=0.5 eg4_resonance1=4 + eg5_time1=0.5 eg5_resonance2=5 + eg6_time1=3 eg6_fil1_gain=-1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createReferenceGraph({ + R"("EG 1 {0}" -> "FilterCutoff {0, N=1}")", + R"("EG 2 {0}" -> "FilterCutoff {0, N=1}")", + R"("EG 3 {0}" -> "FilterResonance {0, N=1}")", + R"("EG 4 {0}" -> "FilterResonance {0, N=1}")", + R"("EG 5 {0}" -> "FilterResonance {0, N=2}")", + R"("EG 6 {0}" -> "FilterGain {0, N=1}")", + })); +} + +TEST_CASE("[Modulations] LFO EQ connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine + lfo1_freq=0.1 lfo1_eq1bw=1 + lfo2_freq=1 lfo2_eq2freq=2 + lfo3_freq=2 lfo3_eq3gain=3 + lfo4_freq=0.5 lfo4_eq3bw=4 + lfo5_freq=0.5 lfo5_eq2gain=5 + lfo6_freq=3 lfo6_eq1freq=-1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createReferenceGraph({ + R"("LFO 1 {0}" -> "EqBandwidth {0, N=1}")", + R"("LFO 2 {0}" -> "EqFrequency {0, N=2}")", + R"("LFO 3 {0}" -> "EqGain {0, N=3}")", + R"("LFO 4 {0}" -> "EqBandwidth {0, N=3}")", + R"("LFO 5 {0}" -> "EqGain {0, N=2}")", + R"("LFO 6 {0}" -> "EqFrequency {0, N=1}")", + })); +} + +TEST_CASE("[Modulations] EG EQ connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine + eg1_freq=0.1 eg1_eq1bw=1 + eg2_freq=1 eg2_eq2freq=2 + eg3_freq=2 eg3_eq3gain=3 + eg4_freq=0.5 eg4_eq3bw=4 + eg5_freq=0.5 eg5_eq2gain=5 + eg6_freq=3 eg6_eq1freq=-1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createReferenceGraph({ + R"("EG 1 {0}" -> "EqBandwidth {0, N=1}")", + R"("EG 2 {0}" -> "EqFrequency {0, N=2}")", + R"("EG 3 {0}" -> "EqGain {0, N=3}")", + R"("EG 4 {0}" -> "EqBandwidth {0, N=3}")", + R"("EG 5 {0}" -> "EqGain {0, N=2}")", + R"("EG 6 {0}" -> "EqFrequency {0, N=1}")", })); } From 2260727a66d7b780ef570caf35b726e89a8e268f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 19 Sep 2020 15:39:46 +0200 Subject: [PATCH 271/445] Aligned vector --- cmake/SfizzConfig.cmake | 4 + external/jsl/LICENSE.md | 23 ++++++ external/jsl/include/jsl/allocator | 73 +++++++++++++++++++ .../jsl/bits/allocator/aligned_allocator.tcc | 41 +++++++++++ .../jsl/bits/allocator/ordinary_allocator.tcc | 53 ++++++++++++++ .../jsl/bits/allocator/stdc_allocator.tcc | 16 ++++ src/CMakeLists.txt | 4 +- src/sfizz/Oversampler.cpp | 10 ++- 8 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 external/jsl/LICENSE.md create mode 100644 external/jsl/include/jsl/allocator create mode 100644 external/jsl/include/jsl/bits/allocator/aligned_allocator.tcc create mode 100644 external/jsl/include/jsl/bits/allocator/ordinary_allocator.tcc create mode 100644 external/jsl/include/jsl/bits/allocator/stdc_allocator.tcc diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 0e70d9e8..0d1c29b4 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -76,6 +76,10 @@ endfunction() # The sndfile library add_library(sfizz-sndfile INTERFACE) +# The jsl utility library for C++ +add_library(sfizz-jsl INTERFACE) +target_include_directories(sfizz-jsl INTERFACE "external/jsl/include") + if (SFIZZ_USE_VCPKG OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") find_package(SndFile CONFIG REQUIRED) find_path(SNDFILE_INCLUDE_DIR sndfile.hh) diff --git a/external/jsl/LICENSE.md b/external/jsl/LICENSE.md new file mode 100644 index 00000000..44da875b --- /dev/null +++ b/external/jsl/LICENSE.md @@ -0,0 +1,23 @@ +# Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/external/jsl/include/jsl/allocator b/external/jsl/include/jsl/allocator new file mode 100644 index 00000000..e97c25a3 --- /dev/null +++ b/external/jsl/include/jsl/allocator @@ -0,0 +1,73 @@ +// -*- C++ -*- +#pragma once +#include +#include + +namespace jsl { + +template +struct ordinary_allocator { + typedef T value_type; + typedef T *pointer; + typedef const T *const_pointer; + typedef T &reference; + typedef const T &const_reference; + typedef std::size_t size_type; + typedef std::ptrdiff_t difference_type; + typedef std::true_type propagate_on_container_move_assignment; + template struct rebind { typedef ordinary_allocator other; }; + typedef std::true_type is_always_equal; + + ordinary_allocator() noexcept {} + ordinary_allocator(const ordinary_allocator &) noexcept {} + template ordinary_allocator(const ordinary_allocator&) noexcept {} + + T *address(T &x) const noexcept; + const T *address(const T &x) const noexcept; + + T *allocate(std::size_t n, const void * = nullptr); + void deallocate(T *p, std::size_t n) noexcept; + std::size_t max_size() const noexcept; + + template void construct(U *p, Args &&...args); + template void destroy(U *p); +}; + +template +inline bool operator==(const ordinary_allocator &, const ordinary_allocator &) noexcept +{ + return true; +} + +template +inline bool operator!=(const ordinary_allocator &, const ordinary_allocator &) noexcept +{ + return false; +} + +//------------------------------------------------------------------------------ + +struct stdc_allocator_traits { + static void *allocate(std::size_t n); + static void deallocate(void *p, std::size_t = 0); +}; + +template +using stdc_allocator = ordinary_allocator; + +//------------------------------------------------------------------------------ + +template +struct aligned_allocator_traits { + static void *allocate(std::size_t n); + static void deallocate(void *p, std::size_t = 0); +}; + +template +using aligned_allocator = ordinary_allocator>; + +} // namespace jsl + +#include "bits/allocator/ordinary_allocator.tcc" +#include "bits/allocator/stdc_allocator.tcc" +#include "bits/allocator/aligned_allocator.tcc" diff --git a/external/jsl/include/jsl/bits/allocator/aligned_allocator.tcc b/external/jsl/include/jsl/bits/allocator/aligned_allocator.tcc new file mode 100644 index 00000000..d42e6585 --- /dev/null +++ b/external/jsl/include/jsl/bits/allocator/aligned_allocator.tcc @@ -0,0 +1,41 @@ +#include "../../allocator" +#include +#if defined(_WIN32) +# include +#else +# include +#endif + +namespace jsl { + +template +void *aligned_allocator_traits::allocate(std::size_t n) +{ + static_assert(Al % sizeof(void *) == 0, + "alignment must be a multiple of the pointer size"); + static_assert((Al & (~Al + 1)) == Al, + "alignment must be a power of two"); +#if defined(_WIN32) + void *p = ::_aligned_malloc(n, Al); + if (!p) + throw std::bad_alloc(); + return p; +#else + void *p; + if (::posix_memalign(&p, Al, n) != 0) + throw std::bad_alloc(); + return p; +#endif +} + +template +void aligned_allocator_traits::deallocate(void *p, std::size_t) +{ +#if defined(_WIN32) + ::_aligned_free(p); +#else + ::free(p); +#endif +} + +} // namespace jsl diff --git a/external/jsl/include/jsl/bits/allocator/ordinary_allocator.tcc b/external/jsl/include/jsl/bits/allocator/ordinary_allocator.tcc new file mode 100644 index 00000000..cfb54b03 --- /dev/null +++ b/external/jsl/include/jsl/bits/allocator/ordinary_allocator.tcc @@ -0,0 +1,53 @@ +#include "../../allocator" +#include + +namespace jsl { + +template +inline T *ordinary_allocator::address(T &x) const noexcept +{ + return &x; +} + +template +inline const T *ordinary_allocator::address(const T &x) const noexcept +{ + return &x; +} + +template +T *ordinary_allocator::allocate(std::size_t n, const void *) +{ + T *ptr = (T *)Traits::allocate(n * sizeof(T)); + if (!ptr) + throw std::bad_alloc(); + return ptr; +} + +template +void ordinary_allocator::deallocate(T *p, std::size_t n) noexcept +{ + Traits::deallocate(p, n * sizeof(T)); +} + +template +std::size_t ordinary_allocator::max_size() const noexcept +{ + return std::numeric_limits::max() / sizeof(T); +} + +template +template +void ordinary_allocator::construct(U *p, Args &&...args) +{ + ::new((void *)p) U(std::forward(args)...); +} + +template +template +void ordinary_allocator::destroy(U *p) +{ + p->~U(); +} + +} // namespace jsl diff --git a/external/jsl/include/jsl/bits/allocator/stdc_allocator.tcc b/external/jsl/include/jsl/bits/allocator/stdc_allocator.tcc new file mode 100644 index 00000000..960ea5ad --- /dev/null +++ b/external/jsl/include/jsl/bits/allocator/stdc_allocator.tcc @@ -0,0 +1,16 @@ +#include "../../allocator" +#include + +namespace jsl { + +inline void *stdc_allocator_traits::allocate(std::size_t n) +{ + return ::malloc(n); +} + +inline void stdc_allocator_traits::deallocate(void *p, std::size_t) +{ + ::free(p); +} + +} // namespace jsl diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7e04487e..2f65ea3c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -216,7 +216,7 @@ target_sources(sfizz_static PRIVATE target_include_directories (sfizz_static PUBLIC .) target_include_directories (sfizz_static PUBLIC external) target_link_libraries (sfizz_static PUBLIC absl::strings absl::span) -target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-atomic) +target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-jsl sfizz-atomic) set_target_properties (sfizz_static PROPERTIES OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp") if (WIN32) target_compile_definitions (sfizz_static PRIVATE _USE_MATH_DEFINES) @@ -255,7 +255,7 @@ if (SFIZZ_SHARED) ${SFIZZ_HEADERS} ${SFIZZ_SOURCES} ${FAUST_FILES} sfizz/sfizz_wrapper.cpp sfizz/sfizz.cpp) target_include_directories (sfizz_shared PRIVATE .) target_include_directories (sfizz_shared PRIVATE external) - target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-atomic) + target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-jsl sfizz-atomic) if (WIN32) target_compile_definitions (sfizz_shared PRIVATE _USE_MATH_DEFINES) endif() diff --git a/src/sfizz/Oversampler.cpp b/src/sfizz/Oversampler.cpp index 8476ff34..4bb1dd4a 100644 --- a/src/sfizz/Oversampler.cpp +++ b/src/sfizz/Oversampler.cpp @@ -9,6 +9,10 @@ #include "AudioSpan.h" #include "AudioReader.h" #include "SIMDConfig.h" +#include + +template +using aligned_vector = std::vector>; constexpr std::array coeffsStage2x { 0.036681502163648017, @@ -70,9 +74,9 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s const auto numFrames = input.getNumFrames(); const auto numChannels = input.getNumChannels(); - std::vector upsampler2x; - std::vector upsampler4x; - std::vector upsampler8x; + aligned_vector upsampler2x; + aligned_vector upsampler4x; + aligned_vector upsampler8x; switch(factor) { From ba03a8f996cb6048f293ec084168388c02957a08 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 19 Sep 2020 16:19:18 +0200 Subject: [PATCH 272/445] Make the methods initCc and initHdcc private --- src/sfizz/Synth.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 278fe312..c308666e 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -369,6 +369,7 @@ public: * @param normValue the normalized cc value, in domain 0 to 1 */ void hdcc(int delay, int ccNumber, float normValue) noexcept; +private: /** * @brief Set the initial value of a controller and send it to the synth * @@ -383,6 +384,7 @@ public: * @param normValue the normalized cc value, in domain 0 to 1 */ void initHdcc(int ccNumber, float normValue) noexcept; +public: /** * @brief Get the initial value of a controller under the current instrument * From d9ba9c879e29778d563ebd91421ba4a2cda57a7a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 20 Sep 2020 15:05:43 +0100 Subject: [PATCH 273/445] Align also in the overload --- src/sfizz/Oversampler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sfizz/Oversampler.cpp b/src/sfizz/Oversampler.cpp index 4bb1dd4a..b4f21621 100644 --- a/src/sfizz/Oversampler.cpp +++ b/src/sfizz/Oversampler.cpp @@ -149,9 +149,9 @@ void sfz::Oversampler::stream(AudioReader& input, AudioSpan output, std:: const auto numFrames = static_cast(input.frames()); const auto numChannels = input.channels(); - std::vector upsampler2x; - std::vector upsampler4x; - std::vector upsampler8x; + aligned_vector upsampler2x; + aligned_vector upsampler4x; + aligned_vector upsampler8x; switch(factor) { From d783ba6c8715ebb19124d713bf3223c9f4946691 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 20 Sep 2020 15:06:01 +0100 Subject: [PATCH 274/445] Make neon required on ARM platforms --- cmake/SfizzConfig.cmake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 0d1c29b4..15376c7d 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -60,6 +60,9 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Werror=return-type) if (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(i.86|x86_64)$") add_compile_options(-msse2) + elseif (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(armv.*)$") + add_compile_options(-mfloat-abi=hard) + add_compile_options(-mfpu=neon) endif() elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") set(CMAKE_CXX_STANDARD 17) From 9415ffe09284dd4b2520614d63cc7dbe966489bb Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sun, 20 Sep 2020 15:19:14 +0100 Subject: [PATCH 275/445] Add clang tidy include flag --- scripts/run_clang_tidy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run_clang_tidy.sh b/scripts/run_clang_tidy.sh index 0f3ff24b..08ceb36c 100755 --- a/scripts/run_clang_tidy.sh +++ b/scripts/run_clang_tidy.sh @@ -30,7 +30,7 @@ clang-tidy \ vst/SfizzVstProcessor.cpp \ vst/SfizzVstEditor.cpp \ vst/SfizzVstState.cpp \ - -- -Iexternal/abseil-cpp -Isrc/external -Isrc/external/pugixml/src \ + -- -Iexternal/abseil-cpp -Iexternal/jsl/include -Isrc/external -Isrc/external/pugixml/src \ -Isrc/sfizz -Isrc -Isrc/external/spline -Isrc/external/cpuid/src \ -Ivst -Ivst/external/VST_SDK/VST3_SDK -Ivst/external/VST_SDK/VST3_SDK/vstgui4 -Ivst/external/ring_buffer \ -Ieditor/src \ From 3bac650498bad367f868fd19847fad12028b3eb9 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 18 Sep 2020 22:04:13 +0100 Subject: [PATCH 276/445] Add a NEON codepath for panning and width --- benchmarks/BM_pan_arm.cpp | 134 +++++++++++++++++++++++++++ benchmarks/CMakeLists.txt | 6 ++ cmake/SfizzConfig.cmake | 4 +- cmake/SfizzSIMDSourceFiles.cmake | 1 + src/sfizz/Panning.cpp | 139 +++++++++++++++++++++++++--- src/sfizz/Panning.h | 13 ++- src/sfizz/effects/Width.cpp | 2 +- src/sfizz/simd/Common.h | 19 +++- src/sfizz/simd/HelpersNEON.cpp | 16 ++++ src/sfizz/simd/HelpersNEON.h | 7 ++ tests/CMakeLists.txt | 2 +- tests/SIMDHelpersT.cpp | 152 ++++++++++++++++++++----------- 12 files changed, 418 insertions(+), 77 deletions(-) create mode 100644 benchmarks/BM_pan_arm.cpp create mode 100644 src/sfizz/simd/HelpersNEON.cpp create mode 100644 src/sfizz/simd/HelpersNEON.h diff --git a/benchmarks/BM_pan_arm.cpp b/benchmarks/BM_pan_arm.cpp new file mode 100644 index 00000000..9ae03b2f --- /dev/null +++ b/benchmarks/BM_pan_arm.cpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Panning.h" +#include "simd/Common.h" +#include +#include +#include +#include "absl/types/span.h" +#include + +#include +template +using aligned_vector = std::vector>; + +// Number of elements in the table, odd for equal volume at center +constexpr int panSize = 4095; + +// Table of pan values for the left channel, extra element for safety +static const auto panData = []() +{ + std::array pan; + int i = 0; + + for (; i < panSize; ++i) + pan[i] = std::cos(i * (piTwo() / (panSize - 1))); + + for (; i < static_cast(pan.size()); ++i) + pan[i] = pan[panSize - 1]; + + return pan; +}(); + +float _panLookup(float pan) +{ + // reduce range, round to nearest + int index = lroundPositive(pan * (panSize - 1)); + return panData[index]; +} + +void panScalar(const float* panEnvelope, float* leftBuffer, float* rightBuffer, unsigned size) noexcept +{ + const auto sentinel = panEnvelope + size; + while (panEnvelope < sentinel) { + auto p =(*panEnvelope + 1.0f) * 0.5f; + p = clamp(p, 0.0f, 1.0f); + *leftBuffer *= _panLookup(p); + *rightBuffer *= _panLookup(1 - p); + incrementAll(panEnvelope, leftBuffer, rightBuffer); + } +} + +void panSIMD(const float* panEnvelope, float* leftBuffer, float* rightBuffer, unsigned size) noexcept +{ + const auto sentinel = panEnvelope + size; + int32_t indices[4]; + while (panEnvelope < sentinel) { + float32x4_t mmPan = vld1q_f32(panEnvelope); + mmPan = vaddq_f32(mmPan, vdupq_n_f32(1.0f)); + mmPan = vmulq_n_f32(mmPan, 0.5f * panSize); + mmPan = vaddq_f32(mmPan, vdupq_n_f32(0.5f)); + mmPan = vminq_f32(mmPan, vdupq_n_f32(panSize)); + mmPan = vmaxq_f32(mmPan, vdupq_n_f32(0.0f)); + int32x4_t mmIdx = vcvtq_s32_f32(mmPan); + vst1q_s32(indices, mmIdx); + + leftBuffer[0] *= panData[indices[0]]; + rightBuffer[0] *= panData[panSize - indices[0] - 1]; + leftBuffer[1] *= panData[indices[1]]; + rightBuffer[1] *= panData[panSize - indices[1]- 1]; + leftBuffer[2] *= panData[indices[2]]; + rightBuffer[2] *= panData[panSize - indices[2]- 1]; + leftBuffer[3] *= panData[indices[3]]; + rightBuffer[3] *= panData[panSize - indices[3]- 1]; + + incrementAll<4>(panEnvelope, leftBuffer, rightBuffer); + } +} + +class PanFixture : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) { + std::random_device rd { }; + std::mt19937 gen { rd() }; + std::uniform_real_distribution dist { -1.0f, 1.0f }; + pan.resize(state.range(0)); + right.resize(state.range(0)); + left.resize(state.range(0)); + + if (!willAlign<16>(pan.data(), left.data(), right.data())) + std::cout << "Will not align!" << '\n'; + absl::c_generate(pan, [&]() { return dist(gen); }); + absl::c_generate(left, [&]() { return dist(gen); }); + absl::c_generate(right, [&]() { return dist(gen); }); + } + + void TearDown(const ::benchmark::State& /* state */) { + + } + + aligned_vector pan; + aligned_vector right; + aligned_vector left; +}; + +BENCHMARK_DEFINE_F(PanFixture, PanScalar)(benchmark::State& state) { + for (auto _ : state) + { + panScalar(pan.data(), left.data(), right.data(), state.range(0)); + } +} + +BENCHMARK_DEFINE_F(PanFixture, PanSIMD)(benchmark::State& state) { + for (auto _ : state) + { + panSIMD(pan.data(), left.data(), right.data(), state.range(0)); + } +} + +BENCHMARK_DEFINE_F(PanFixture, PanSfizz)(benchmark::State& state) { + for (auto _ : state) + { + sfz::pan(pan.data(), left.data(), right.data(), state.range(0)); + } +} + +// Register the function as a benchmark +BENCHMARK_REGISTER_F(PanFixture, PanScalar)->RangeMultiplier(4)->Range((1 << 4), (1 << 12)); +BENCHMARK_REGISTER_F(PanFixture, PanSIMD)->RangeMultiplier(4)->Range((1 << 4), (1 << 12)); +BENCHMARK_REGISTER_F(PanFixture, PanSfizz)->RangeMultiplier(4)->Range((1 << 4), (1 << 12)); +BENCHMARK_MAIN(); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 0ecd4494..d53ff15f 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -147,6 +147,12 @@ if (TARGET bm_resample) add_dependencies(sfizz_benchmarks bm_resample) endif() +if (SFIZZ_SYSTEM_PROCESSOR MATCHES "armv7l") + sfizz_add_benchmark(bm_pan_arm BM_pan_arm.cpp ../src/sfizz/Panning.cpp) + target_link_libraries(bm_pan_arm PRIVATE sfizz-jsl) + add_dependencies(sfizz_benchmarks bm_pan_arm) +endif() + configure_file("sample.wav" "${CMAKE_BINARY_DIR}/benchmarks/sample1.wav" COPYONLY) configure_file("sample.wav" "${CMAKE_BINARY_DIR}/benchmarks/sample2.wav" COPYONLY) configure_file("sample.wav" "${CMAKE_BINARY_DIR}/benchmarks/sample3.wav" COPYONLY) diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 15376c7d..95b0adaa 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -60,9 +60,9 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Werror=return-type) if (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(i.86|x86_64)$") add_compile_options(-msse2) - elseif (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(armv.*)$") - add_compile_options(-mfloat-abi=hard) + elseif(SFIZZ_SYSTEM_PROCESSOR MATCHES "^(arm.*)$") add_compile_options(-mfpu=neon) + add_compile_options(-mfloat-abi=hard) endif() elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") set(CMAKE_CXX_STANDARD 17) diff --git a/cmake/SfizzSIMDSourceFiles.cmake b/cmake/SfizzSIMDSourceFiles.cmake index 3cc9afdd..83957c5d 100644 --- a/cmake/SfizzSIMDSourceFiles.cmake +++ b/cmake/SfizzSIMDSourceFiles.cmake @@ -3,6 +3,7 @@ macro(sfizz_add_simd_sources SOURCES_VAR PREFIX) list (APPEND ${SOURCES_VAR} ${PREFIX}/sfizz/SIMDHelpers.cpp + ${PREFIX}/sfizz/simd/HelpersNEON.cpp ${PREFIX}/sfizz/simd/HelpersSSE.cpp ${PREFIX}/sfizz/simd/HelpersAVX.cpp) diff --git a/src/sfizz/Panning.cpp b/src/sfizz/Panning.cpp index 299f2955..f7b96bd9 100644 --- a/src/sfizz/Panning.cpp +++ b/src/sfizz/Panning.cpp @@ -1,11 +1,21 @@ #include "Panning.h" +#include "MathHelpers.h" #include #include +#if SFIZZ_HAVE_NEON +#include +#include "simd/Common.h" +using Type = float; +constexpr unsigned TypeAlignment = 4; +constexpr unsigned ByteAlignment = TypeAlignment * sizeof(Type); +#endif + + namespace sfz { -// Number of elements in the table, odd for equal volume at center -constexpr int panSize = 4095; + +constexpr int panSize { 4095 }; // Table of pan values for the left channel, extra element for safety static const auto panData = []() @@ -25,35 +35,134 @@ static const auto panData = []() float panLookup(float pan) { // reduce range, round to nearest - int index = lroundPositive(pan * (panSize - 1)); + const int index = lroundPositive(pan * (panSize - 1)); return panData[index]; } +inline void tickPan(const float* pan, float* leftBuffer, float* rightBuffer) +{ + auto p = (*pan + 1.0f) * 0.5f; + p = clamp(p, 0.0f, 1.0f); + *leftBuffer *= panLookup(p); + *rightBuffer *= panLookup(1 - p); +} + void pan(const float* panEnvelope, float* leftBuffer, float* rightBuffer, unsigned size) noexcept { const auto sentinel = panEnvelope + size; + +#if SFIZZ_HAVE_NEON + const auto firstAligned = prevAligned(panEnvelope + TypeAlignment - 1); + + if (willAlign(panEnvelope, leftBuffer, rightBuffer) && (firstAligned < sentinel)) { + while (panEnvelope < firstAligned) { + tickPan(panEnvelope, leftBuffer, rightBuffer); + incrementAll(panEnvelope, leftBuffer, rightBuffer); + } + + uint32_t indices[TypeAlignment]; + float leftPan[TypeAlignment]; + float rightPan[TypeAlignment]; + const auto lastAligned = prevAligned(sentinel); + while (panEnvelope < lastAligned) { + float32x4_t mmPan = vld1q_f32(panEnvelope); + mmPan = vaddq_f32(mmPan, vdupq_n_f32(1.0f)); + mmPan = vmulq_n_f32(mmPan, 0.5f * panSize); + mmPan = vaddq_f32(mmPan, vdupq_n_f32(0.5f)); + uint32x4_t mmIdx = vcvtq_u32_f32(mmPan); + mmIdx = vminq_u32(mmIdx, vdupq_n_u32(panSize - 1)); + mmIdx = vmaxq_u32(mmIdx, vdupq_n_u32(0)); + vst1q_u32(indices, mmIdx); + + leftPan[0] = panData[indices[0]]; + rightPan[0] = panData[panSize - indices[0] - 1]; + leftPan[1] = panData[indices[1]]; + rightPan[1] = panData[panSize - indices[1] - 1]; + leftPan[2] = panData[indices[2]]; + rightPan[2] = panData[panSize - indices[2] - 1]; + leftPan[3] = panData[indices[3]]; + rightPan[3] = panData[panSize - indices[3] - 1]; + + vst1q_f32(leftBuffer, vmulq_f32(vld1q_f32(leftBuffer), vld1q_f32(leftPan))); + vst1q_f32(rightBuffer, vmulq_f32(vld1q_f32(rightBuffer), vld1q_f32(rightPan))); + + incrementAll(panEnvelope, leftBuffer, rightBuffer); + } + } +#endif + while (panEnvelope < sentinel) { - auto p =(*panEnvelope + 1.0f) * 0.5f; - p = clamp(p, 0.0f, 1.0f); - *leftBuffer *= panLookup(p); - *rightBuffer *= panLookup(1 - p); + tickPan(panEnvelope, leftBuffer, rightBuffer); incrementAll(panEnvelope, leftBuffer, rightBuffer); } + +} + +inline void tickWidth(const float* width, float* leftBuffer, float* rightBuffer) +{ + float w = (*width + 1.0f) * 0.5f; + w = clamp(w, 0.0f, 1.0f); + const auto coeff1 = panLookup(w); + const auto coeff2 = panLookup(1 - w); + const auto l = *leftBuffer; + const auto r = *rightBuffer; + *leftBuffer = l * coeff2 + r * coeff1; + *rightBuffer = l * coeff1 + r * coeff2; } void width(const float* widthEnvelope, float* leftBuffer, float* rightBuffer, unsigned size) noexcept { const auto sentinel = widthEnvelope + size; + +#if SFIZZ_HAVE_NEON + const auto firstAligned = prevAligned(widthEnvelope + TypeAlignment - 1); + + if (willAlign(widthEnvelope, leftBuffer, rightBuffer) && firstAligned < sentinel) { + while (widthEnvelope < firstAligned) { + tickWidth(widthEnvelope, leftBuffer, rightBuffer); + incrementAll(widthEnvelope, leftBuffer, rightBuffer); + } + + uint32_t indices[TypeAlignment]; + float coeff1[TypeAlignment]; + float coeff2[TypeAlignment]; + const auto lastAligned = prevAligned(sentinel); + while (widthEnvelope < lastAligned) { + float32x4_t mmWidth = vld1q_f32(widthEnvelope); + mmWidth = vaddq_f32(mmWidth, vdupq_n_f32(1.0f)); + mmWidth = vmulq_n_f32(mmWidth, 0.5f * panSize); + mmWidth = vaddq_f32(mmWidth, vdupq_n_f32(0.5f)); + uint32x4_t mmIdx = vcvtq_u32_f32(mmWidth); + mmIdx = vminq_u32(mmIdx, vdupq_n_u32(panSize - 1)); + mmIdx = vmaxq_u32(mmIdx, vdupq_n_u32(0)); + vst1q_u32(indices, mmIdx); + + coeff1[0] = panData[indices[0]]; + coeff2[0] = panData[panSize - indices[0] - 1]; + coeff1[1] = panData[indices[1]]; + coeff2[1] = panData[panSize - indices[1] - 1]; + coeff1[2] = panData[indices[2]]; + coeff2[2] = panData[panSize - indices[2] - 1]; + coeff1[3] = panData[indices[3]]; + coeff2[3] = panData[panSize - indices[3] - 1]; + + float32x4_t mmCoeff1 = vld1q_f32(coeff1); + float32x4_t mmCoeff2 = vld1q_f32(coeff2); + float32x4_t mmLeft = vld1q_f32(leftBuffer); + float32x4_t mmRight = vld1q_f32(rightBuffer); + + vst1q_f32(leftBuffer, vaddq_f32(vmulq_f32(mmCoeff2, mmLeft), vmulq_f32(mmCoeff1, mmRight))); + vst1q_f32(rightBuffer, vaddq_f32(vmulq_f32(mmCoeff1, mmLeft), vmulq_f32(mmCoeff2, mmRight))); + + incrementAll(widthEnvelope, leftBuffer, rightBuffer); + } + } +#endif // SFIZZ_HAVE_NEON + while (widthEnvelope < sentinel) { - float w = (*widthEnvelope + 1.0f) * 0.5f; - w = clamp(w, 0.0f, 1.0f); - const auto coeff1 = panLookup(w); - const auto coeff2 = panLookup(1 - w); - const auto l = *leftBuffer; - const auto r = *rightBuffer; - *leftBuffer = l * coeff2 + r * coeff1; - *rightBuffer = l * coeff1 + r * coeff2; + tickWidth(widthEnvelope, leftBuffer, rightBuffer); incrementAll(widthEnvelope, leftBuffer, rightBuffer); } } + } diff --git a/src/sfizz/Panning.h b/src/sfizz/Panning.h index 75d31ea4..eddb4d48 100644 --- a/src/sfizz/Panning.h +++ b/src/sfizz/Panning.h @@ -6,13 +6,16 @@ namespace sfz { /** - * @brief Lookup a value from the pan table - * - * @param pan - * @return float - */ +* @brief Lookup a value from the pan table +* No check is done on the range, needs to be capped +* between 0 and panSize. +* +* @param pan +* @return float +*/ float panLookup(float pan); + /** * @brief Pans a mono signal left or right * diff --git a/src/sfizz/effects/Width.cpp b/src/sfizz/effects/Width.cpp index 52d2876b..a9c3c202 100644 --- a/src/sfizz/effects/Width.cpp +++ b/src/sfizz/effects/Width.cpp @@ -15,8 +15,8 @@ */ #include "Width.h" -#include "Opcode.h" #include "Panning.h" +#include "Opcode.h" #include "absl/memory/memory.h" namespace sfz { diff --git a/src/sfizz/simd/Common.h b/src/sfizz/simd/Common.h index 6fdddeab..493f46f8 100644 --- a/src/sfizz/simd/Common.h +++ b/src/sfizz/simd/Common.h @@ -24,7 +24,7 @@ T* prevAligned(const T* ptr) template bool unaligned(const T* ptr) { - return (reinterpret_cast(ptr) & ByteAlignmentMask(N) )!= 0; + return (reinterpret_cast(ptr) & ByteAlignmentMask(N) ) != 0; } template @@ -32,3 +32,20 @@ bool unaligned(const T* ptr1, Args... rest) { return unaligned(ptr1) || unaligned(rest...); } + +template +bool willAlign(const T* ptr1, const T* ptr2) +{ + const auto p1 = reinterpret_cast(ptr1); + const auto p2 = reinterpret_cast(ptr2); + return ( + (p1 & ByteAlignmentMask(N)) == (p2 & ByteAlignmentMask(N)) + && ((p1 & ByteAlignmentMask(sizeof(T))) == 0) + ); +} + +template +bool willAlign(const T* ptr1, const T* ptr2, Args... rest) +{ + return willAlign(ptr1, ptr2) && willAlign(ptr2, rest...); +} diff --git a/src/sfizz/simd/HelpersNEON.cpp b/src/sfizz/simd/HelpersNEON.cpp new file mode 100644 index 00000000..746deb5e --- /dev/null +++ b/src/sfizz/simd/HelpersNEON.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "HelpersNEON.h" +#include "Common.h" + +#if SFIZZ_HAVE_NEON +#include +#endif + +using Type = float; +constexpr unsigned TypeAlignment = 4; +constexpr unsigned ByteAlignment = TypeAlignment * sizeof(Type); diff --git a/src/sfizz/simd/HelpersNEON.h b/src/sfizz/simd/HelpersNEON.h new file mode 100644 index 00000000..2746084a --- /dev/null +++ b/src/sfizz/simd/HelpersNEON.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0090620c..eba64aea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -46,7 +46,7 @@ set(SFIZZ_TEST_SOURCES ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) -target_link_libraries(sfizz_tests PRIVATE sfizz::sfizz) +target_link_libraries(sfizz_tests PRIVATE sfizz::sfizz sfizz-jsl) sfizz_enable_lto_if_needed(sfizz_tests) sfizz_enable_fast_math(sfizz_tests) # target_link_libraries(sfizz_tests PRIVATE absl::strings absl::str_format absl::flat_hash_map cnpy absl::span absl::algorithm) diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index 27502881..bbf9b70b 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -4,6 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +#include "sfizz/simd/Common.h" #include "sfizz/SIMDHelpers.h" #include "sfizz/Panning.h" #include "catch2/catch.hpp" @@ -12,8 +13,12 @@ #include #include #include +#include using namespace Catch::literals; +template +using aligned_vector = std::vector>; + constexpr int smallBufferSize { 3 }; constexpr int bigBufferSize { 4095 }; constexpr int medBufferSize { 127 }; @@ -49,6 +54,40 @@ inline bool approxEqual(absl::Span lhs, absl::Span rhs, return true; } +TEST_CASE("[Helpers] willAlign, prevAligned and unaligned tests") +{ + aligned_vector array(16); + REQUIRE( !unaligned<16>(&array[0]) ); + REQUIRE( !unaligned<16>(&array[4]) ); + REQUIRE( !unaligned<32>(&array[8]) ); + REQUIRE( unaligned<32>(&array[7]) ); + REQUIRE( unaligned<32>(&array[4]) ); + REQUIRE( unaligned<16>(&array[3]) ); + REQUIRE( !unaligned<16>(&array[0], &array[4]) ); + REQUIRE( !unaligned<16>(&array[0], &array[4], &array[8]) ); + REQUIRE( unaligned<16>(&array[0], &array[3], &array[8]) ); + + REQUIRE( prevAligned<16>(&array[0]) == &array[0] ); + REQUIRE( prevAligned<16>(&array[1]) == &array[0] ); + REQUIRE( prevAligned<16>(&array[2]) == &array[0] ); + REQUIRE( prevAligned<16>(&array[3]) == &array[0] ); + REQUIRE( prevAligned<16>(&array[4]) == &array[4] ); + REQUIRE( prevAligned<16>(&array[5]) == &array[4] ); + REQUIRE( prevAligned<32>(&array[7]) == &array[0] ); + REQUIRE( prevAligned<32>(&array[8]) == &array[8] ); + REQUIRE( prevAligned<32>(&array[9]) == &array[8] ); + + REQUIRE( willAlign<16>(&array[0], &array[4]) ); + REQUIRE( willAlign<16>(&array[5], &array[1]) ); + REQUIRE( !willAlign<16>(&array[2], &array[1]) ); + REQUIRE( willAlign<32>(&array[9], &array[1]) ); + REQUIRE( willAlign<32>(&array[8], &array[0]) ); + + float* meanPointer = (float*)((uint8_t*)&array[1] + 1); + REQUIRE( !willAlign<16>(&array[0], meanPointer) ); + REQUIRE( !willAlign<16>(&array[4], &array[0], meanPointer) ); +} + TEST_CASE("[Helpers] Interleaved read") { std::array input { 0.0f, 10.0f, 1.0f, 11.0f, 2.0f, 12.0f, 3.0f, 13.0f, 4.0f, 14.0f, 5.0f, 15.0f, 6.0f, 16.0f, 7.0f, 17.0f }; @@ -834,62 +873,71 @@ TEST_CASE("[Helpers] Diff (SIMD vs Scalar)") REQUIRE(approxEqual(outputScalar, outputSIMD)); } -TEST_CASE("[Helpers] Pan Scalar") +template +void panTest(float leftValue, float rightValue, float panValue, float expectedLeft, float expectedRight) { - std::array leftValue { 1.0f }; - std::array rightValue { 1.0f }; - auto left = absl::MakeSpan(leftValue); - auto right = absl::MakeSpan(rightValue); - SECTION("Pan = 0") - { - std::array pan { 0.0f }; - sfz::pan(pan, left, right); - REQUIRE(left[0] == Approx(0.70711f).margin(0.001f)); - REQUIRE(right[0] == Approx(0.70711f).margin(0.001f)); - } - SECTION("Pan = 1") - { - std::array pan { 1.0f }; - sfz::pan(pan, left, right); - REQUIRE(left[0] == Approx(0.0f).margin(0.001f)); - REQUIRE(right[0] == Approx(1.0f).margin(0.001f)); - } - SECTION("Pan = -1") - { - std::array pan { -1.0f }; - sfz::pan(pan, left, right); - REQUIRE(left[0] == Approx(1.0f).margin(0.001f)); - REQUIRE(right[0] == Approx(0.0f).margin(0.001f)); - } + std::vector leftChannel(N); + std::vector rightChannel(N); + std::vector pan(N); + std::vector expectedLeftChannel(N); + std::vector expectedRightChannel(N); + std::fill(leftChannel.begin(), leftChannel.end(), leftValue); + std::fill(expectedLeftChannel.begin(), expectedLeftChannel.end(), expectedLeft); + std::fill(rightChannel.begin(), rightChannel.end(), rightValue); + std::fill(expectedRightChannel.begin(), expectedRightChannel.end(), expectedRight); + std::fill(pan.begin(), pan.end(), panValue); + auto left = absl::MakeSpan(leftChannel); + auto right = absl::MakeSpan(rightChannel); + sfz::pan(pan, left, right); + REQUIRE_THAT( leftChannel, Catch::Approx(expectedLeftChannel).margin(0.001) ); + REQUIRE_THAT( rightChannel, Catch::Approx(expectedRightChannel).margin(0.001) ); } -TEST_CASE("[Helpers] Width Scalar") +template +void widthTest(float leftValue, float rightValue, float widthValue, float expectedLeft, float expectedRight) { - std::array leftValue { 1.0f }; - std::array rightValue { 1.0f }; - auto left = absl::MakeSpan(leftValue); - auto right = absl::MakeSpan(rightValue); - SECTION("width = 1") - { - std::array width { 1.0f }; - sfz::width(width, left, right); - REQUIRE(left[0] == Approx(1.0f).margin(0.001f)); - REQUIRE(right[0] == Approx(1.0f).margin(0.001f)); - } - SECTION("width = 0") - { - std::array width { 0.0f }; - sfz::width(width, left, right); - REQUIRE(left[0] == Approx(1.414f).margin(0.001f)); - REQUIRE(right[0] == Approx(1.414f).margin(0.001f)); - } - SECTION("width = -1") - { - std::array width { -1.0f }; - sfz::width(width, left, right); - REQUIRE(left[0] == Approx(1.0f).margin(0.001f)); - REQUIRE(right[0] == Approx(1.0f).margin(0.001f)); - } + std::vector leftChannel(N); + std::vector rightChannel(N); + std::vector width(N); + std::vector expectedLeftChannel(N); + std::vector expectedRightChannel(N); + std::fill(leftChannel.begin(), leftChannel.end(), leftValue); + std::fill(expectedLeftChannel.begin(), expectedLeftChannel.end(), expectedLeft); + std::fill(rightChannel.begin(), rightChannel.end(), rightValue); + std::fill(expectedRightChannel.begin(), expectedRightChannel.end(), expectedRight); + std::fill(width.begin(), width.end(), widthValue); + auto left = absl::MakeSpan(leftChannel); + auto right = absl::MakeSpan(rightChannel); + sfz::width(width, left, right); + REQUIRE_THAT( leftChannel, Catch::Approx(expectedLeftChannel).margin(0.001) ); + REQUIRE_THAT( rightChannel, Catch::Approx(expectedRightChannel).margin(0.001) ); +} + +TEST_CASE("[Helpers] Pan tests") +{ + // Testing different sizes to check that SIMD and unrolling works as expected + panTest<1>(1.0f, 1.0f, 0.0f, 0.70711f, 0.70711f); + panTest<1>(1.0f, 1.0f, 1.0f, 0.0f, 1.0f); + panTest<1>(1.0f, 1.0f, -1.0f, 1.0f, 0.0f); + panTest<3>(1.0f, 1.0f, 0.0f, 0.70711f, 0.70711f); + panTest<3>(1.0f, 1.0f, 1.0f, 0.0f, 1.0f); + panTest<3>(1.0f, 1.0f, -1.0f, 1.0f, 0.0f); + panTest<10>(1.0f, 1.0f, 0.0f, 0.70711f, 0.70711f); + panTest<10>(1.0f, 1.0f, 1.0f, 0.0f, 1.0f); + panTest<10>(1.0f, 1.0f, -1.0f, 1.0f, 0.0f); +} + +TEST_CASE("[Helpers] Width tests") +{ + widthTest<1>(1.0f, 1.0f, 0.0f, 1.414f, 1.414f); + widthTest<1>(1.0f, 1.0f, 1.0f, 1.0f, 1.0f); + widthTest<1>(1.0f, 1.0f, -1.0f, 1.0f, 1.0f); + widthTest<3>(1.0f, 1.0f, 0.0f, 1.414f, 1.414f); + widthTest<3>(1.0f, 1.0f, 1.0f, 1.0f, 1.0f); + widthTest<3>(1.0f, 1.0f, -1.0f, 1.0f, 1.0f); + widthTest<10>(1.0f, 1.0f, 0.0f, 1.414f, 1.414f); + widthTest<10>(1.0f, 1.0f, 1.0f, 1.0f, 1.0f); + widthTest<10>(1.0f, 1.0f, -1.0f, 1.0f, 1.0f); } TEST_CASE("[Helpers] clampAll") From 27866af6113729941d936416a577366692d38362 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 20 Sep 2020 21:27:46 +0200 Subject: [PATCH 277/445] Initialize the file chooser with the directory of the current file --- editor/src/editor/Editor.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index ba16d43f..43b012bb 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -11,6 +11,7 @@ #include "NativeHelpers.h" #include #include +#include #include #include #include @@ -30,6 +31,7 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { SharedPointer mainView_; std::string currentSfzFile_; + std::string currentScalaFile_; enum { kPanelGeneral, @@ -226,6 +228,7 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) case EditId::ScalaFile: { const std::string& value = v.to_string(); + currentScalaFile_ = value; updateScalaFileLabel(value); } break; @@ -663,6 +666,10 @@ void Editor::Impl::chooseSfzFile() fs->setTitle("Load SFZ file"); fs->setDefaultExtension(CFileExtension("SFZ", "sfz")); + if (!currentSfzFile_.empty()) { + std::string initialDir = fs::path(currentSfzFile_).parent_path().u8string() + '/'; + fs->setInitialDirectory(initialDir.c_str()); + } if (fs->runModal()) { UTF8StringPtr file = fs->getSelectedFile(0); @@ -684,6 +691,10 @@ void Editor::Impl::chooseScalaFile() fs->setTitle("Load Scala file"); fs->setDefaultExtension(CFileExtension("SCL", "scl")); + if (!currentScalaFile_.empty()) { + std::string initialDir = fs::path(currentScalaFile_).parent_path().u8string() + '/'; + fs->setInitialDirectory(initialDir.c_str()); + } if (fs->runModal()) { UTF8StringPtr file = fs->getSelectedFile(0); @@ -695,6 +706,7 @@ void Editor::Impl::chooseScalaFile() void Editor::Impl::changeScalaFile(const std::string& filePath) { ctrl_->uiSendValue(EditId::ScalaFile, filePath); + currentScalaFile_ = filePath; updateScalaFileLabel(filePath); } From 80924acf7a447ff75b66368a1e447ed902fbb4e4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 21 Sep 2020 11:47:21 +0200 Subject: [PATCH 278/445] Adjust the level of *noise to match ARIA --- src/sfizz/Config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index e56548fc..54ba4871 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -81,7 +81,7 @@ namespace config { constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; constexpr int oscillatorsPerVoice { 9 }; - constexpr float uniformNoiseBounds { 0.25f }; + constexpr float uniformNoiseBounds { 1.0f }; constexpr float noiseVariance { 0.25f }; /** Minimum interval in frames between recomputations of coefficients of the From 6494cdac105349e5787cca3ba9ac796d34753f07 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 21 Sep 2020 20:19:14 +0200 Subject: [PATCH 279/445] Update vstgui to use vfork --- editor/external/vstgui4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 index d412207f..a8a546b8 160000 --- a/editor/external/vstgui4 +++ b/editor/external/vstgui4 @@ -1 +1 @@ -Subproject commit d412207f5b013a3f572c6b67d75b788401369496 +Subproject commit a8a546b89ebef7e263125b2f5859a3a365884cc6 From 85883481c996d3886115bc6a429ca7f088471606 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 15:53:33 +0200 Subject: [PATCH 280/445] Auto-enable oscillator=on when a wavetable is provided --- src/sfizz/ADSREnvelope.cpp | 2 +- src/sfizz/Config.h | 5 ++ src/sfizz/FilePool.cpp | 13 +++- src/sfizz/FilePool.h | 2 + src/sfizz/Region.cpp | 2 +- src/sfizz/Region.h | 21 +++++- src/sfizz/Synth.cpp | 19 +++--- src/sfizz/Voice.cpp | 52 +++++++-------- tests/FilesT.cpp | 103 ++++++++++++++++++++++------- tests/TestFiles/channels_multi.sfz | 4 ++ 10 files changed, 154 insertions(+), 69 deletions(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index 419a8f55..803adbb6 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -55,7 +55,7 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, shouldRelease = false; freeRunning = ( (this->sustain == 0.0f) - || (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator)) + || (region.loopMode == SfzLoopMode::one_shot && region.isOscillator()) ); currentValue = this->start; currentState = State::Delay; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 54ba4871..442a165c 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -113,6 +113,11 @@ namespace config { static constexpr double amplitudeTriangle = 1.0; static constexpr double amplitudeSaw = 0.8164965809277261; // sqrt(2)/sqrt(3) static constexpr double amplitudeSquare = 0.8164965809277261; // should have been sqrt(2)? + /** + Frame count high limit, for automatically loading a sound file as wavetable. + Set to 3000 according to Cakewalk. + */ + static constexpr unsigned wavetableMaxFrames = 3000; /** Background file loading */ diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 6e27153b..8b4ca8a3 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -25,7 +25,6 @@ #include "FilePool.h" #include "AudioReader.h" -#include "FileMetadata.h" #include "Buffer.h" #include "AudioBuffer.h" #include "AudioSpan.h" @@ -218,13 +217,21 @@ absl::optional sfz::FilePool::getFileInformation(const Fil SF_INSTRUMENT instrumentInfo {}; + FileMetadataReader mdReader; + bool mdReaderOpened = mdReader.open(file); + if (!reader->getInstrument(&instrumentInfo)) { // if no instrument, then try extracting from embedded RIFF chunks (flac) - FileMetadataReader mdReader; - if (mdReader.open(file)) + if (mdReaderOpened) mdReader.extractRiffInstrument(instrumentInfo); } + if (mdReaderOpened) { + WavetableInfo wt; + if (mdReader.extractWavetableInfo(wt)) + returnedValue.wavetable = wt; + } + if (!fileId.isReverse()) { if (instrumentInfo.loop_count > 0) { returnedValue.hasLoop = true; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 9d099550..dcef362c 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -31,6 +31,7 @@ #include "AudioBuffer.h" #include "AudioSpan.h" #include "FileId.h" +#include "FileMetadata.h" #include "SIMDHelpers.h" #include "utility/SpinMutex.h" #include "ghc/fs_std.hpp" @@ -55,6 +56,7 @@ struct FileInformation { bool hasLoop { false }; double sampleRate { config::defaultSampleRate }; int numChannels { 0 }; + absl::optional wavetable; }; // Strict C++11 disallows member initialization if aggregate initialization is to be used... diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index f2097395..7ec6619d 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -145,7 +145,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) break; case hash("oscillator"): if (auto value = readBooleanFromOpcode(opcode)) - oscillator = *value; + oscillatorEnabled = *value ? OscillatorEnabled::On : OscillatorEnabled::Off; break; case hash("oscillator_multi"): setValueFromOpcode(opcode, oscillatorMulti, Default::oscillatorMultiRange); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 4c9dabba..1d402bbe 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -76,13 +76,28 @@ struct Region { * @return false */ bool isGenerator() const noexcept { return sampleId.filename().size() > 0 ? sampleId.filename()[0] == '*' : false; } + /** + * @brief Is an oscillator (generator or wavetable)? + * + * @return true + * @return false + */ + bool isOscillator() const noexcept + { + if (isGenerator()) + return true; + else if (oscillatorEnabled != OscillatorEnabled::Auto) + return oscillatorEnabled == OscillatorEnabled::On; + else + return hasWavetableSample; + } /** * @brief Is stereo (has stereo sample or is unison oscillator)? * * @return true * @return false */ - bool isStereo() const noexcept { return hasStereoSample || ((oscillator || isGenerator()) && oscillatorMulti >= 3); } + bool isStereo() const noexcept { return hasStereoSample || (isOscillator() && oscillatorMulti >= 3); } /** * @brief Is a looping region (at least potentially)? * @@ -284,7 +299,9 @@ struct Region { // Wavetable oscillator float oscillatorPhase { Default::oscillatorPhase }; - bool oscillator = false; + enum class OscillatorEnabled { Auto = -1, Off = 0, On = 1 }; + OscillatorEnabled oscillatorEnabled = OscillatorEnabled::Auto; // oscillator + bool hasWavetableSample = false; // (set according to sample file) int oscillatorMulti = Default::oscillatorMulti; float oscillatorDetune = Default::oscillatorDetune; absl::optional oscillatorQuality; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 02335014..e63803c1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -495,18 +495,25 @@ void sfz::Synth::finalizeSfzLoad() while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); - if (!region->oscillator && !region->isGenerator()) { + absl::optional fileInformation; + + if (!region->isGenerator()) { if (!resources.filePool.checkSampleId(region->sampleId)) { removeCurrentRegion(); continue; } - const auto fileInformation = resources.filePool.getFileInformation(region->sampleId); + fileInformation = resources.filePool.getFileInformation(region->sampleId); if (!fileInformation) { removeCurrentRegion(); continue; } + region->hasWavetableSample = fileInformation->wavetable || + fileInformation->end < config::wavetableMaxFrames; + } + + if (!region->isOscillator()) { region->sampleEnd = std::min(region->sampleEnd, fileInformation->end); if (fileInformation->hasLoop) { @@ -539,12 +546,8 @@ void sfz::Synth::finalizeSfzLoad() if (!resources.filePool.preloadFile(region->sampleId, maxOffset)) removeCurrentRegion(); - } else if (region->oscillator && !region->isGenerator()) { - if (!resources.filePool.checkSampleId(region->sampleId)) { - removeCurrentRegion(); - continue; - } - + } + else if (!region->isGenerator()) { if (!resources.wavePool.createFileWave(resources.filePool, std::string(region->sampleId.filename()))) { removeCurrentRegion(); continue; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index d080db37..499e21b2 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -58,25 +58,29 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event if (delay < 0) delay = 0; - if (region->isGenerator()) { + if (region->isOscillator()) { const WavetableMulti* wave = nullptr; - switch (hash(region->sampleId.filename())) { - default: - case hash("*silence"): - break; - case hash("*sine"): - wave = resources.wavePool.getWaveSin(); - break; - case hash("*triangle"): // fallthrough - case hash("*tri"): - wave = resources.wavePool.getWaveTriangle(); - break; - case hash("*square"): - wave = resources.wavePool.getWaveSquare(); - break; - case hash("*saw"): - wave = resources.wavePool.getWaveSaw(); - break; + if (!region->isGenerator()) + wave = resources.wavePool.getFileWave(region->sampleId.filename()); + else { + switch (hash(region->sampleId.filename())) { + default: + case hash("*silence"): + break; + case hash("*sine"): + wave = resources.wavePool.getWaveSin(); + break; + case hash("*triangle"): // fallthrough + case hash("*tri"): + wave = resources.wavePool.getWaveTriangle(); + break; + case hash("*square"): + wave = resources.wavePool.getWaveSquare(); + break; + case hash("*saw"): + wave = resources.wavePool.getWaveSaw(); + break; + } } const float phase = region->getPhase(); const int quality = region->oscillatorQuality.value_or(Default::oscillatorQuality); @@ -86,16 +90,6 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event osc.setQuality(quality); } setupOscillatorUnison(); - } else if (region->oscillator) { - const WavetableMulti* wave = resources.wavePool.getFileWave(region->sampleId.filename()); - const float phase = region->getPhase(); - const int quality = region->oscillatorQuality.value_or(Default::oscillatorQuality); - for (WavetableOscillator& osc : waveOscillators) { - osc.setWavetable(wave); - osc.setPhase(phase); - osc.setQuality(quality); - } - setupOscillatorUnison(); } else { currentPromise = resources.filePool.getFilePromise(region->sampleId); if (currentPromise == nullptr) { @@ -276,7 +270,7 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept { // Fill buffer with raw data ScopedTiming logger { dataDuration }; - if (region->isGenerator() || region->oscillator) + if (region->isOscillator()) fillWithGenerator(delayed_buffer); else fillWithData(delayed_buffer); diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index e5a7c77f..d0d0b064 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -270,37 +270,90 @@ TEST_CASE("[Files] Channels (channels_multi.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels_multi.sfz"); - REQUIRE(synth.getNumRegions() == 6); + REQUIRE(synth.getNumRegions() == 10); - REQUIRE(synth.getRegionView(0)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(0)->isStereo()); - REQUIRE(synth.getRegionView(0)->isGenerator()); - REQUIRE(!synth.getRegionView(0)->oscillator); + int regionNumber = 0; + const Region* region = nullptr; - REQUIRE(synth.getRegionView(1)->sampleId.filename() == "*sine"); - REQUIRE(synth.getRegionView(1)->isStereo()); - REQUIRE(synth.getRegionView(1)->isGenerator()); - REQUIRE(!synth.getRegionView(1)->oscillator); + // generator only + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(!region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); - REQUIRE(synth.getRegionView(2)->sampleId.filename() == "ramp_wave.wav"); - REQUIRE(!synth.getRegionView(2)->isStereo()); - REQUIRE(!synth.getRegionView(2)->isGenerator()); - REQUIRE(synth.getRegionView(2)->oscillator); + // generator with multi + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); - REQUIRE(synth.getRegionView(3)->sampleId.filename() == "ramp_wave.wav"); - REQUIRE(synth.getRegionView(3)->isStereo()); - REQUIRE(!synth.getRegionView(3)->isGenerator()); - REQUIRE(synth.getRegionView(3)->oscillator); + // explicit wavetable + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::On); - REQUIRE(synth.getRegionView(4)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(4)->isStereo()); - REQUIRE(synth.getRegionView(4)->isGenerator()); - REQUIRE(!synth.getRegionView(4)->oscillator); + // explicit wavetable with multi + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::On); - REQUIRE(synth.getRegionView(5)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(5)->isStereo()); - REQUIRE(synth.getRegionView(5)->isGenerator()); - REQUIRE(!synth.getRegionView(5)->oscillator); + // explicit disabled wavetable + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(!region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Off); + + // explicit disabled wavetable with multi + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(!region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Off); + + // implicit wavetable (sound file < 3000 frames) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); + + // implicit non-wavetable (sound file >= 3000 frames) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "snare.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(!region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); + + // generator with multi=1 (single) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(!region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); + + // generator with multi=2 (ring modulation) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(!region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); } TEST_CASE("[Files] sw_default") diff --git a/tests/TestFiles/channels_multi.sfz b/tests/TestFiles/channels_multi.sfz index 8146ba45..ee6d6acf 100644 --- a/tests/TestFiles/channels_multi.sfz +++ b/tests/TestFiles/channels_multi.sfz @@ -2,5 +2,9 @@ sample=*sine oscillator_multi=3 sample=ramp_wave.wav oscillator=on sample=ramp_wave.wav oscillator=on oscillator_multi=3 + sample=ramp_wave.wav oscillator=off + sample=ramp_wave.wav oscillator=off oscillator_multi=3 + sample=ramp_wave.wav + sample=snare.wav sample=*sine oscillator_multi=1 sample=*sine oscillator_multi=2 From 5d4a3741b33c0bd038663bfbc9c669afc20cc54b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 16:42:02 +0200 Subject: [PATCH 281/445] Fix the build on Windows --- src/sfizz/FileMetadata.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sfizz/FileMetadata.h b/src/sfizz/FileMetadata.h index f4e30bd7..9e93be2e 100644 --- a/src/sfizz/FileMetadata.h +++ b/src/sfizz/FileMetadata.h @@ -6,10 +6,14 @@ #pragma once #include "ghc/fs_std.hpp" -#include #include #include #include +#if defined(_WIN32) +#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 +#include +#endif +#include namespace sfz { From 5c1931de2c4c6a16c3d6c7f32c4294cc882b58f2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 18:23:31 +0200 Subject: [PATCH 282/445] Add oscillator_detune_oncc --- src/sfizz/Defaults.h | 1 + src/sfizz/Region.cpp | 3 +++ src/sfizz/Voice.cpp | 26 ++++++++++++++++++++------ src/sfizz/Voice.h | 1 + src/sfizz/Wavetables.cpp | 18 +++++++++--------- src/sfizz/Wavetables.h | 6 +++--- src/sfizz/modulations/ModId.cpp | 2 ++ src/sfizz/modulations/ModId.h | 1 + src/sfizz/modulations/ModKey.cpp | 2 ++ tests/DemoWavetables.cpp | 8 +++++++- 10 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index e6cc47f5..1fb2255f 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -68,6 +68,7 @@ namespace Default constexpr Range oscillatorMultiRange { 1, config::oscillatorsPerVoice }; constexpr float oscillatorDetune { 0 }; constexpr Range oscillatorDetuneRange { -9600, 9600 }; + constexpr Range oscillatorDetuneCCRange { -9600, 9600 }; constexpr int oscillatorQuality { 1 }; constexpr Range oscillatorQualityRange { 0, 3 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 7ec6619d..affc4997 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -153,6 +153,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("oscillator_detune"): setValueFromOpcode(opcode, oscillatorDetune, Default::oscillatorDetuneRange); break; + case_any_ccN("oscillator_detune"): + processGenericCc(opcode, Default::oscillatorDetuneCCRange, ModKey::createNXYZ(ModId::OscillatorDetune, id)); + break; case hash("oscillator_quality"): if (opcode.value == "-1") oscillatorQuality.reset(); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 499e21b2..ad236fe7 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -669,9 +669,14 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept fill(*frequencies, pitchRatio * keycenterFrequency); pitchEnvelope(*frequencies); + auto detuneSpan = resources.bufferPool.getBuffer(numFrames); + if (!detuneSpan) + return; + if (waveUnisonSize == 1) { WavetableOscillator& osc = waveOscillators[0]; - osc.processModulated(frequencies->data(), 1.0, leftSpan.data(), buffer.getNumFrames()); + fill(*detuneSpan, 1.0f); + osc.processModulated(frequencies->data(), detuneSpan->data(), leftSpan.data(), buffer.getNumFrames()); copy(leftSpan, rightSpan); } else { @@ -681,11 +686,19 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept if (!tempSpan) return; - for (unsigned i = 0, n = waveUnisonSize; i < n; ++i) { - WavetableOscillator& osc = waveOscillators[i]; - osc.processModulated(frequencies->data(), waveDetuneRatio[i], tempSpan->data(), numFrames); - multiplyAdd1(waveLeftGain[i], *tempSpan, leftSpan); - multiplyAdd1(waveRightGain[i], *tempSpan, rightSpan); + const float* detuneMod = resources.modMatrix.getModulation(oscillatorDetuneTarget); + for (unsigned u = 0, uSize = waveUnisonSize; u < uSize; ++u) { + WavetableOscillator& osc = waveOscillators[u]; + if (!detuneMod) + fill(*detuneSpan, waveDetuneRatio[u]); + else { + for (size_t i = 0; i < numFrames; ++i) + (*detuneSpan)[i] = centsFactor(detuneMod[i]); + applyGain1(waveDetuneRatio[u], *detuneSpan); + } + osc.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), numFrames); + multiplyAdd1(waveLeftGain[u], *tempSpan, leftSpan); + multiplyAdd1(waveRightGain[u], *tempSpan, rightSpan); } } } @@ -921,4 +934,5 @@ void sfz::Voice::saveModulationTargets(const Region* region) noexcept positionTarget = mm.findTarget(ModKey::createNXYZ(ModId::Position, region->getId())); widthTarget = mm.findTarget(ModKey::createNXYZ(ModId::Width, region->getId())); pitchTarget = mm.findTarget(ModKey::createNXYZ(ModId::Pitch, region->getId())); + oscillatorDetuneTarget = mm.findTarget(ModKey::createNXYZ(ModId::OscillatorDetune, region->getId())); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 8afe3a06..4224c927 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -494,6 +494,7 @@ private: ModMatrix::TargetId positionTarget; ModMatrix::TargetId widthTarget; ModMatrix::TargetId pitchTarget; + ModMatrix::TargetId oscillatorDetuneTarget; PowerFollower powerFollower; diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index 55a39be6..2a668a13 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -60,7 +60,7 @@ void WavetableOscillator::processSingle(float frequency, float detuneRatio, floa } template -void WavetableOscillator::processModulatedSingle(const float* frequencies, float detuneRatio, float* output, unsigned nframes) +void WavetableOscillator::processModulatedSingle(const float* frequencies, const float* detuneRatios, float* output, unsigned nframes) { float phase = _phase; float sampleInterval = _sampleInterval; @@ -70,7 +70,7 @@ void WavetableOscillator::processModulatedSingle(const float* frequencies, float for (unsigned i = 0; i < nframes; ++i) { float frequency = frequencies[i]; - float phaseInc = frequency * (detuneRatio * sampleInterval); + float phaseInc = frequency * (detuneRatios[i] * sampleInterval); absl::Span table = multi.getTableForFrequency(frequency); float position = phase * tableSize; @@ -111,7 +111,7 @@ void WavetableOscillator::processDual(float frequency, float detuneRatio, float* } template -void WavetableOscillator::processModulatedDual(const float* frequencies, float detuneRatio, float* output, unsigned nframes) +void WavetableOscillator::processModulatedDual(const float* frequencies, const float* detuneRatios, float* output, unsigned nframes) { float phase = _phase; float sampleInterval = _sampleInterval; @@ -121,7 +121,7 @@ void WavetableOscillator::processModulatedDual(const float* frequencies, float d for (unsigned i = 0; i < nframes; ++i) { float frequency = frequencies[i]; - float phaseInc = frequency * (detuneRatio * sampleInterval); + float phaseInc = frequency * (detuneRatios[i] * sampleInterval); WavetableMulti::DualTable dt = multi.getInterpolationPairForFrequency(frequency); @@ -159,22 +159,22 @@ void WavetableOscillator::process(float frequency, float detuneRatio, float* out } } -void WavetableOscillator::processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes) +void WavetableOscillator::processModulated(const float* frequencies, const float* detuneRatios, float* output, unsigned nframes) { int quality = clamp(_quality, 0, 3); switch (quality) { case 0: - processModulatedSingle(frequencies, detuneRatio, output, nframes); + processModulatedSingle(frequencies, detuneRatios, output, nframes); break; case 1: - processModulatedSingle(frequencies, detuneRatio, output, nframes); + processModulatedSingle(frequencies, detuneRatios, output, nframes); break; case 2: - processModulatedSingle(frequencies, detuneRatio, output, nframes); + processModulatedSingle(frequencies, detuneRatios, output, nframes); break; case 3: - processModulatedDual(frequencies, detuneRatio, output, nframes); + processModulatedDual(frequencies, detuneRatios, output, nframes); break; } } diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index 87cd72aa..a7d90773 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -71,20 +71,20 @@ public: /** Compute a cycle of the oscillator, with varying frequency. */ - void processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes); + void processModulated(const float* frequencies, const float* detuneRatios, float* output, unsigned nframes); private: // single-table interpolation template void processSingle(float frequency, float detuneRatio, float* output, unsigned nframes); template - void processModulatedSingle(const float* frequencies, float detuneRatio, float* output, unsigned nframes); + void processModulatedSingle(const float* frequencies, const float* detuneRatios, float* output, unsigned nframes); // dual-table interpolation template void processDual(float frequency, float detuneRatio, float* output, unsigned nframes); template - void processModulatedDual(const float* frequencies, float detuneRatio, float* output, unsigned nframes); + void processModulatedDual(const float* frequencies, const float* detuneRatios, float* output, unsigned nframes); private: float _phase = 0.0f; diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index 9d19e10e..ed1c6fc0 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -56,6 +56,8 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice|kModIsAdditive; case ModId::EqBandwidth: return kModIsPerVoice|kModIsAdditive; + case ModId::OscillatorDetune: + return kModIsPerVoice|kModIsAdditive; // unknown default: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index 7ab96ee9..f7402e39 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -43,6 +43,7 @@ enum class ModId : int { EqGain, EqFrequency, EqBandwidth, + OscillatorDetune, _TargetsEnd, // [/targets] -------------------------------------------------------------- diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 3615c976..7a7a3a3c 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -99,6 +99,8 @@ std::string ModKey::toString() const return absl::StrCat("EqFrequency {", region_.number(), ", N=", 1 + params_.N, "}"); case ModId::EqBandwidth: return absl::StrCat("EqBandwidth {", region_.number(), ", N=", 1 + params_.N, "}"); + case ModId::OscillatorDetune: + return absl::StrCat("OscillatorDetune {", region_.number(), ", N=", 1 + params_.N, "}"); default: return {}; diff --git a/tests/DemoWavetables.cpp b/tests/DemoWavetables.cpp index 873eb68d..bd57abbe 100644 --- a/tests/DemoWavetables.cpp +++ b/tests/DemoWavetables.cpp @@ -58,6 +58,7 @@ private: float fSweepIncrement = 0.0; std::unique_ptr fTmpFrequency; + std::unique_ptr fTmpDetune; jack_client_u fClient; jack_port_t* fPorts[2] = {}; @@ -88,6 +89,7 @@ bool DemoApp::initSound() unsigned bufferSize = jack_get_buffer_size(client); fTmpFrequency.reset(new float[bufferSize]); + fTmpDetune.reset(new float[bufferSize]); fMulti[0] = sfz::WavetableMulti::createForHarmonicProfile( sfz::HarmonicProfile::getSine(), sfz::config::amplitudeSine, 2048); @@ -188,8 +190,12 @@ int DemoApp::processAudio(jack_nframes_t nframes, void* cbdata) } self->fSweepCurrent = sweepCurrent; + // fill the detune value + float* detune = self->fTmpDetune.get(); + std::fill(detune, detune + nframes, 1.0f); + // compute oscillator - osc.processModulated(frequency, 1.0, left, nframes); + osc.processModulated(frequency, detune, left, nframes); std::memcpy(right, left, nframes * sizeof(float)); return 0; From 3b3f6d2731027a9bb8fe24bd5488c146083f57b4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 13 Aug 2020 20:57:51 +0200 Subject: [PATCH 283/445] Set the pitch key center from sample --- src/sfizz/FilePool.cpp | 10 +++++++--- src/sfizz/FilePool.h | 1 + src/sfizz/Region.cpp | 7 ++++++- src/sfizz/Region.h | 1 + src/sfizz/Synth.cpp | 3 +++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 8b4ca8a3..9750da2c 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -216,14 +216,15 @@ absl::optional sfz::FilePool::getFileInformation(const Fil returnedValue.numChannels = reader->channels(); SF_INSTRUMENT instrumentInfo {}; + bool haveInstrumentInfo = reader->getInstrument(&instrumentInfo); FileMetadataReader mdReader; bool mdReaderOpened = mdReader.open(file); - if (!reader->getInstrument(&instrumentInfo)) { + if (!haveInstrumentInfo) { // if no instrument, then try extracting from embedded RIFF chunks (flac) if (mdReaderOpened) - mdReader.extractRiffInstrument(instrumentInfo); + haveInstrumentInfo = mdReader.extractRiffInstrument(instrumentInfo); } if (mdReaderOpened) { @@ -233,7 +234,7 @@ absl::optional sfz::FilePool::getFileInformation(const Fil } if (!fileId.isReverse()) { - if (instrumentInfo.loop_count > 0) { + if (haveInstrumentInfo && instrumentInfo.loop_count > 0) { returnedValue.hasLoop = true; returnedValue.loopBegin = instrumentInfo.loops[0].start; returnedValue.loopEnd = min(returnedValue.end, instrumentInfo.loops[0].end - 1); @@ -243,6 +244,9 @@ absl::optional sfz::FilePool::getFileInformation(const Fil // prehaps it can make use of SF_LOOP_BACKWARD? } + if (haveInstrumentInfo) + returnedValue.rootKey = clamp(instrumentInfo.basenote, 0, 127); + return returnedValue; } diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index dcef362c..f05712fd 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -56,6 +56,7 @@ struct FileInformation { bool hasLoop { false }; double sampleRate { config::defaultSampleRate }; int numChannels { 0 }; + int rootKey { 0 }; absl::optional wavetable; }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 7ec6619d..83417fb5 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -779,7 +779,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) // Performance parameters: pitch case hash("pitch_keycenter"): - setValueFromOpcode(opcode, pitchKeycenter, Default::keyRange); + if (opcode.value == "sample") + pitchKeycenterFromSample = true; + else { + pitchKeycenterFromSample = false; + setValueFromOpcode(opcode, pitchKeycenter, Default::keyRange); + } break; case hash("pitch_keytrack"): setValueFromOpcode(opcode, pitchKeytrack, Default::pitchKeytrackRange); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 1d402bbe..61d97b27 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -382,6 +382,7 @@ struct Region { // Performance parameters: pitch uint8_t pitchKeycenter { Default::pitchKeycenter }; // pitch_keycenter + bool pitchKeycenterFromSample { false }; int pitchKeytrack { Default::pitchKeytrack }; // pitch_keytrack int pitchRandom { Default::pitchRandom }; // pitch_random int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e63803c1..4dd86f5e 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -536,6 +536,9 @@ void sfz::Synth::finalizeSfzLoad() if (fileInformation->numChannels == 2) region->hasStereoSample = true; + if (region->pitchKeycenterFromSample) + region->pitchKeycenter = fileInformation->rootKey; + // TODO: adjust with LFO targets const auto maxOffset = [region]() { uint64_t sumOffsetCC = region->offset + region->offsetRandom; From ffb8e2c52a2bda9bfd6a3993945e8c7efc9c5c5a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 14 Aug 2020 00:35:50 +0200 Subject: [PATCH 284/445] Add tests --- tests/FilesT.cpp | 18 ++++++++++++++++++ tests/TestFiles/root_key_38.flac | Bin 0 -> 8436 bytes tests/TestFiles/root_key_38.wav | Bin 0 -> 90 bytes tests/TestFiles/root_key_62.flac | Bin 0 -> 8436 bytes tests/TestFiles/root_key_62.wav | Bin 0 -> 90 bytes 5 files changed, 18 insertions(+) create mode 100644 tests/TestFiles/root_key_38.flac create mode 100644 tests/TestFiles/root_key_38.wav create mode 100644 tests/TestFiles/root_key_62.flac create mode 100644 tests/TestFiles/root_key_62.wav diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index d0d0b064..f4662417 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -684,3 +684,21 @@ TEST_CASE("[Files] Duplicate labels") REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); } + +TEST_CASE("[Files] Key center from audio file") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sample_keycenter.sfz", R"( + pitch_keycenter=sample + sample=root_key_38.wav + sample=root_key_62.wav + sample=root_key_38.flac + sample=root_key_62.flac + )"); + + REQUIRE(synth.getNumRegions() == 4); + REQUIRE(synth.getRegionView(0)->pitchKeycenter == 38); + REQUIRE(synth.getRegionView(1)->pitchKeycenter == 62); + REQUIRE(synth.getRegionView(2)->pitchKeycenter == 38); + REQUIRE(synth.getRegionView(3)->pitchKeycenter == 62); +} diff --git a/tests/TestFiles/root_key_38.flac b/tests/TestFiles/root_key_38.flac new file mode 100644 index 0000000000000000000000000000000000000000..d8eefb36ad9420da8054eb2edc8bac8162343d5e GIT binary patch literal 8436 zcmeI&v1$S_7zgnG)ecf_2+kFPl2W*^I`xJwd1bGx-~3Y5ulVn_>;X_oY)rQm1YL>y_1>rC zo^8%d7TUhRk>1<^>wzQZm96pZ{(LeUzt~GIZ8HE~|AU)ZYFu0M6)r+qV+DP4f*}Pc zKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=EsWC~!K)X&@`#0knGt Da9${I literal 0 HcmV?d00001 diff --git a/tests/TestFiles/root_key_38.wav b/tests/TestFiles/root_key_38.wav new file mode 100644 index 0000000000000000000000000000000000000000..e7a27089c869484b53a1de2bf5911108f4c2f0ba GIT binary patch literal 90 zcmWIYbaM-0U|(DONqvt{8rWE`a-H|XL! zbn0954LZ8DUqA=%4TOIZNWL%mLio+GNj3m*uApS{Wt^;Tx3ctBs^7S}e12biMe9xP z-W~!C$^UKM5zy3xlfO}l5hrsK*E096*&ro%W+B_4$j zcal4GIXkPgdx0lCS^^(qPtGe>W8D7R>~(tKF8OJnW8mgDsLfL2yPBu0%4Cf>bnOH~ z3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1^%PJ?z=b!&g46Q F_V;zmD2M<6 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/root_key_62.wav b/tests/TestFiles/root_key_62.wav new file mode 100644 index 0000000000000000000000000000000000000000..658d5bd4d6d4510e649e6061feb7258c926a0d23 GIT binary patch literal 90 zcmWIYbaM-0U| Date: Fri, 14 Aug 2020 03:13:31 +0200 Subject: [PATCH 285/445] Support overriding with `key` opcode --- src/sfizz/Region.cpp | 1 + tests/FilesT.cpp | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 83417fb5..daffdb4f 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -232,6 +232,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setRangeStartFromOpcode(opcode, keyRange, Default::keyRange); setRangeEndFromOpcode(opcode, keyRange, Default::keyRange); setValueFromOpcode(opcode, pitchKeycenter, Default::keyRange); + pitchKeycenterFromSample = false; break; case hash("lovel"): if (auto value = readOpcode(opcode.value, Default::midi7Range)) diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index f4662417..ef605365 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -694,11 +694,13 @@ TEST_CASE("[Files] Key center from audio file") sample=root_key_62.wav sample=root_key_38.flac sample=root_key_62.flac + key=10 sample=root_key_62.flac )"); - REQUIRE(synth.getNumRegions() == 4); + REQUIRE(synth.getNumRegions() == 5); REQUIRE(synth.getRegionView(0)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(1)->pitchKeycenter == 62); REQUIRE(synth.getRegionView(2)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(3)->pitchKeycenter == 62); + REQUIRE(synth.getRegionView(4)->pitchKeycenter == 10); } From fdfcee78f6b547293b33fdcd32c0710790285a1a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 20:33:57 +0200 Subject: [PATCH 286/445] Make sure the test files are not processed as wavetables --- tests/FilesT.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index ef605365..7f56511b 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -689,7 +689,7 @@ TEST_CASE("[Files] Key center from audio file") { sfz::Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/sample_keycenter.sfz", R"( - pitch_keycenter=sample + pitch_keycenter=sample oscillator=off sample=root_key_38.wav sample=root_key_62.wav sample=root_key_38.flac From c5a1d04fb74c488fe9b4dd7d616cc43371789297 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 20:36:48 +0200 Subject: [PATCH 287/445] Make the `key` opcode not take effect on sample keycenter --- src/sfizz/Region.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index daffdb4f..83417fb5 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -232,7 +232,6 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setRangeStartFromOpcode(opcode, keyRange, Default::keyRange); setRangeEndFromOpcode(opcode, keyRange, Default::keyRange); setValueFromOpcode(opcode, pitchKeycenter, Default::keyRange); - pitchKeycenterFromSample = false; break; case hash("lovel"): if (auto value = readOpcode(opcode.value, Default::midi7Range)) From bd6a5af03536f141cddf5297bd5cddb568a0ded6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 20:39:17 +0200 Subject: [PATCH 288/445] Update tests for new keycenter behavior --- tests/FilesT.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 7f56511b..dbff1ba1 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -694,13 +694,15 @@ TEST_CASE("[Files] Key center from audio file") sample=root_key_62.wav sample=root_key_38.flac sample=root_key_62.flac + pitch_keycenter=10 sample=root_key_62.flac key=10 sample=root_key_62.flac )"); - REQUIRE(synth.getNumRegions() == 5); + REQUIRE(synth.getNumRegions() == 6); REQUIRE(synth.getRegionView(0)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(1)->pitchKeycenter == 62); REQUIRE(synth.getRegionView(2)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(3)->pitchKeycenter == 62); REQUIRE(synth.getRegionView(4)->pitchKeycenter == 10); + REQUIRE(synth.getRegionView(5)->pitchKeycenter == 62); } From 05b5728d88b67da4005643f17d8204b0a5c3076e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 21:49:22 +0200 Subject: [PATCH 289/445] Implement RM synthesis --- src/sfizz/Defaults.h | 5 ++ src/sfizz/Region.cpp | 10 +++ src/sfizz/Region.h | 2 + src/sfizz/Voice.cpp | 103 ++++++++++++++++++++++++++----- src/sfizz/Voice.h | 1 + src/sfizz/modulations/ModId.cpp | 2 + src/sfizz/modulations/ModId.h | 1 + src/sfizz/modulations/ModKey.cpp | 2 + 8 files changed, 111 insertions(+), 15 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 1fb2255f..df71352e 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -64,11 +64,16 @@ namespace Default // Wavetable oscillator constexpr float oscillatorPhase { 0.0 }; constexpr Range oscillatorPhaseRange { -1.0, 360.0 }; + constexpr int oscillatorMode { 0 }; constexpr int oscillatorMulti { 1 }; + constexpr Range oscillatorModeRange { 0, 2 }; constexpr Range oscillatorMultiRange { 1, config::oscillatorsPerVoice }; constexpr float oscillatorDetune { 0 }; constexpr Range oscillatorDetuneRange { -9600, 9600 }; constexpr Range oscillatorDetuneCCRange { -9600, 9600 }; + constexpr float oscillatorModDepth { 0 }; + constexpr Range oscillatorModDepthRange { 0, 100 }; + constexpr Range oscillatorModDepthCCRange { 0, 100 }; constexpr int oscillatorQuality { 1 }; constexpr Range oscillatorQualityRange { 0, 3 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index affc4997..4f6fc2a9 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -147,6 +147,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (auto value = readBooleanFromOpcode(opcode)) oscillatorEnabled = *value ? OscillatorEnabled::On : OscillatorEnabled::Off; break; + case hash("oscillator_mode"): + setValueFromOpcode(opcode, oscillatorMode, Default::oscillatorModeRange); + break; case hash("oscillator_multi"): setValueFromOpcode(opcode, oscillatorMulti, Default::oscillatorMultiRange); break; @@ -156,6 +159,13 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case_any_ccN("oscillator_detune"): processGenericCc(opcode, Default::oscillatorDetuneCCRange, ModKey::createNXYZ(ModId::OscillatorDetune, id)); break; + case hash("oscillator_mod_depth"): + if (auto value = readOpcode(opcode.value, Default::oscillatorModDepthRange)) + oscillatorModDepth = normalizePercents(*value); + break; + case_any_ccN("oscillator_mod_depth"): + processGenericCc(opcode, Default::oscillatorModDepthCCRange, ModKey::createNXYZ(ModId::OscillatorModDepth, id)); + break; case hash("oscillator_quality"): if (opcode.value == "-1") oscillatorQuality.reset(); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 1d402bbe..083fdd7a 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -302,8 +302,10 @@ struct Region { enum class OscillatorEnabled { Auto = -1, Off = 0, On = 1 }; OscillatorEnabled oscillatorEnabled = OscillatorEnabled::Auto; // oscillator bool hasWavetableSample = false; // (set according to sample file) + int oscillatorMode = Default::oscillatorMode; int oscillatorMulti = Default::oscillatorMulti; float oscillatorDetune = Default::oscillatorDetune; + float oscillatorModDepth = Default::oscillatorModDepth; absl::optional oscillatorQuality; // Instrument settings: voice lifecycle diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index ad236fe7..8d5479e2 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -661,6 +661,8 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept } else { const auto numFrames = buffer.getNumFrames(); + buffer.fill(0.0f); + auto frequencies = resources.bufferPool.getBuffer(numFrames); if (!frequencies) return; @@ -673,19 +675,28 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept if (!detuneSpan) return; - if (waveUnisonSize == 1) { - WavetableOscillator& osc = waveOscillators[0]; - fill(*detuneSpan, 1.0f); - osc.processModulated(frequencies->data(), detuneSpan->data(), leftSpan.data(), buffer.getNumFrames()); - copy(leftSpan, rightSpan); - } - else { - buffer.fill(0.0f); + const int oscillatorMode = region->oscillatorMode; + const int oscillatorMulti = region->oscillatorMulti; + if (oscillatorMode <= 0 && oscillatorMulti < 2) { + // single oscillator auto tempSpan = resources.bufferPool.getBuffer(numFrames); if (!tempSpan) return; + WavetableOscillator& osc = waveOscillators[0]; + fill(*detuneSpan, 1.0f); + osc.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), buffer.getNumFrames()); + copy(*tempSpan, leftSpan); + copy(*tempSpan, rightSpan); + } + else if (oscillatorMode <= 0 && oscillatorMulti >= 3) { + // unison oscillator + auto tempSpan = resources.bufferPool.getBuffer(numFrames); + auto temp2Span = resources.bufferPool.getBuffer(numFrames); + if (!tempSpan || !temp2Span) + return; + const float* detuneMod = resources.modMatrix.getModulation(oscillatorDetuneTarget); for (unsigned u = 0, uSize = waveUnisonSize; u < uSize; ++u) { WavetableOscillator& osc = waveOscillators[u]; @@ -697,10 +708,66 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept applyGain1(waveDetuneRatio[u], *detuneSpan); } osc.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), numFrames); - multiplyAdd1(waveLeftGain[u], *tempSpan, leftSpan); - multiplyAdd1(waveRightGain[u], *tempSpan, rightSpan); + multiplyAdd1(waveLeftGain[u], *tempSpan, *temp2Span); + copy(*temp2Span, leftSpan); + multiplyAdd1(waveRightGain[u], *tempSpan, *temp2Span); + copy(*temp2Span, rightSpan); } } + else { + // modulated oscillator + auto tempSpan = resources.bufferPool.getBuffer(numFrames); + if (!tempSpan) + return; + + WavetableOscillator& oscCar = waveOscillators[0]; + WavetableOscillator& oscMod = waveOscillators[1]; + + // compute the modulator + auto modulatorSpan = resources.bufferPool.getBuffer(numFrames); + if (!modulatorSpan) + return; + + const float* detuneMod = resources.modMatrix.getModulation(oscillatorDetuneTarget); + if (!detuneMod) + fill(*detuneSpan, waveDetuneRatio[1]); + else { + for (size_t i = 0; i < numFrames; ++i) + (*detuneSpan)[i] = centsFactor(detuneMod[i]); + applyGain1(waveDetuneRatio[1], *detuneSpan); + } + + oscMod.processModulated(frequencies->data(), detuneSpan->data(), modulatorSpan->data(), numFrames); + + // scale the modulator + const float oscillatorModDepth = region->oscillatorModDepth; + if (oscillatorModDepth != 1.0f) + applyGain1(oscillatorModDepth, *modulatorSpan); + const float* modDepthMod = resources.modMatrix.getModulation(oscillatorModDepthTarget); + if (modDepthMod) + multiplyMul1(0.01f, absl::MakeConstSpan(modDepthMod, numFrames), *modulatorSpan); + + // compute carrier×modulator + switch (region->oscillatorMode) { + case 0: // RM synthesis + default: + fill(*detuneSpan, 1.0f); + oscCar.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), buffer.getNumFrames()); + applyGain(*modulatorSpan, *tempSpan); + break; + + case 1: // PM synthesis + return; // Note(jpc): not yet implemented + break; + + case 2: // FM synthesis + return; // Note(jpc): not yet implemented + break; + } + + copy(*tempSpan, leftSpan); + copy(*tempSpan, rightSpan); + } } #if 0 @@ -828,16 +895,21 @@ void sfz::Voice::setMaxFlexEGsPerVoice(size_t numFlexEGs) void sfz::Voice::setupOscillatorUnison() { - int m = region->oscillatorMulti; - float d = region->oscillatorDetune; + const int m = region->oscillatorMulti; + const float d = region->oscillatorDetune; // 3-9: unison mode, 1: normal/RM, 2: PM/FM - // TODO(jpc) RM/FM/PM synthesis - if (m < 3) { + if (m < 3 || region->oscillatorMode > 0) { waveUnisonSize = 1; + // carrier waveDetuneRatio[0] = 1.0; waveLeftGain[0] = 1.0; waveRightGain[0] = 1.0; + // modulator + const float modDepth = region->oscillatorModDepth; + waveDetuneRatio[1] = centsFactor(d); + waveLeftGain[1] = modDepth; + waveRightGain[1] = modDepth; return; } @@ -856,7 +928,7 @@ void sfz::Voice::setupOscillatorUnison() // detune (ratio) for (int i = 0; i < m; ++i) - waveDetuneRatio[i] = std::exp2(detunes[i] * (0.01f / 12.0f)); + waveDetuneRatio[i] = centsFactor(detunes[i]); // gains waveLeftGain[0] = 0.0; @@ -935,4 +1007,5 @@ void sfz::Voice::saveModulationTargets(const Region* region) noexcept widthTarget = mm.findTarget(ModKey::createNXYZ(ModId::Width, region->getId())); pitchTarget = mm.findTarget(ModKey::createNXYZ(ModId::Pitch, region->getId())); oscillatorDetuneTarget = mm.findTarget(ModKey::createNXYZ(ModId::OscillatorDetune, region->getId())); + oscillatorModDepthTarget = mm.findTarget(ModKey::createNXYZ(ModId::OscillatorModDepth, region->getId())); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 4224c927..348022f6 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -495,6 +495,7 @@ private: ModMatrix::TargetId widthTarget; ModMatrix::TargetId pitchTarget; ModMatrix::TargetId oscillatorDetuneTarget; + ModMatrix::TargetId oscillatorModDepthTarget; PowerFollower powerFollower; diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index ed1c6fc0..f294a0bc 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -58,6 +58,8 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice|kModIsAdditive; case ModId::OscillatorDetune: return kModIsPerVoice|kModIsAdditive; + case ModId::OscillatorModDepth: + return kModIsPerVoice|kModIsPercentMultiplicative; // unknown default: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index f7402e39..daf7f72f 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -44,6 +44,7 @@ enum class ModId : int { EqFrequency, EqBandwidth, OscillatorDetune, + OscillatorModDepth, _TargetsEnd, // [/targets] -------------------------------------------------------------- diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 7a7a3a3c..efb6c8a0 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -101,6 +101,8 @@ std::string ModKey::toString() const return absl::StrCat("EqBandwidth {", region_.number(), ", N=", 1 + params_.N, "}"); case ModId::OscillatorDetune: return absl::StrCat("OscillatorDetune {", region_.number(), ", N=", 1 + params_.N, "}"); + case ModId::OscillatorModDepth: + return absl::StrCat("OscillatorModDepth {", region_.number(), ", N=", 1 + params_.N, "}"); default: return {}; From a587b4035b2d217301142973520af72caad63155 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 00:20:32 +0200 Subject: [PATCH 290/445] Give room for FM index greater than 1 --- src/sfizz/Defaults.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index df71352e..dc54a6c2 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -72,8 +72,8 @@ namespace Default constexpr Range oscillatorDetuneRange { -9600, 9600 }; constexpr Range oscillatorDetuneCCRange { -9600, 9600 }; constexpr float oscillatorModDepth { 0 }; - constexpr Range oscillatorModDepthRange { 0, 100 }; - constexpr Range oscillatorModDepthCCRange { 0, 100 }; + constexpr Range oscillatorModDepthRange { 0, 10000 }; // depth%, allowed to be >100 for FM + constexpr Range oscillatorModDepthCCRange { 0, 10000 }; constexpr int oscillatorQuality { 1 }; constexpr Range oscillatorQualityRange { 0, 3 }; From e19c20e90ef8a9e78a878fc0c64b2c63e4d7c0f9 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 01:09:37 +0200 Subject: [PATCH 291/445] Allow oscillator frequency to be modulated into negatives --- src/sfizz/Wavetables.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index 2a668a13..c8cba91c 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -36,6 +36,14 @@ void WavetableOscillator::setPhase(float phase) _phase = phase; } +static float incrementAndWrap(float phase, float inc) +{ + phase += inc; + phase -= static_cast(phase); + phase += phase < 0.0f; // in case of negative frequencies + return phase; +} + template void WavetableOscillator::processSingle(float frequency, float detuneRatio, float* output, unsigned nframes) { @@ -52,8 +60,7 @@ void WavetableOscillator::processSingle(float frequency, float detuneRatio, floa float frac = position - index; output[i] = interpolate(&table[index], frac); - phase += phaseInc; - phase -= static_cast(phase); + phase = incrementAndWrap(phase, phaseInc); } _phase = phase; @@ -78,8 +85,7 @@ void WavetableOscillator::processModulatedSingle(const float* frequencies, const float frac = position - index; output[i] = interpolate(&table[index], frac); - phase += phaseInc; - phase -= static_cast(phase); + phase = incrementAndWrap(phase, phaseInc); } _phase = phase; @@ -103,8 +109,7 @@ void WavetableOscillator::processDual(float frequency, float detuneRatio, float* (1 - dt.delta) * interpolate(&dt.table1[index], frac) + dt.delta * interpolate(&dt.table2[index], frac); - phase += phaseInc; - phase -= static_cast(phase); + phase = incrementAndWrap(phase, phaseInc); } _phase = phase; @@ -132,8 +137,7 @@ void WavetableOscillator::processModulatedDual(const float* frequencies, const f (1 - dt.delta) * interpolate(&dt.table1[index], frac) + dt.delta * interpolate(&dt.table2[index], frac); - phase += phaseInc; - phase -= static_cast(phase); + phase = incrementAndWrap(phase, phaseInc); } _phase = phase; From 2b2a373b13985b8931f76db52149c910d2dcc9f8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 01:10:24 +0200 Subject: [PATCH 292/445] Add FM synthesis --- src/sfizz/Voice.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 8d5479e2..ae49816a 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -757,11 +757,15 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept break; case 1: // PM synthesis - return; // Note(jpc): not yet implemented + // Note(jpc): not implemented, just do FM instead + goto fm_synthesis; break; case 2: // FM synthesis - return; // Note(jpc): not yet implemented + fm_synthesis: + fill(*detuneSpan, 1.0f); + multiplyAdd(*modulatorSpan, *frequencies, *frequencies); + oscCar.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), buffer.getNumFrames()); break; } From 1bb2fbdf400ffc49d420d9f6b0f5a465be47c391 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 23 Sep 2020 13:27:51 +0200 Subject: [PATCH 293/445] Add a default ampeg_release --- src/sfizz/Defaults.h | 1 + src/sfizz/Region.h | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index e6cc47f5..8972f513 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -231,6 +231,7 @@ namespace Default constexpr float delayEG { 0 }; constexpr float hold { 0 }; constexpr float release { 0 }; + constexpr float ampegRelease { 0.001 }; // Default release to avoid clicks constexpr float vel2release { 0.0f }; constexpr float start { 0.0 }; constexpr float sustain { 100.0 }; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 61d97b27..8023070c 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -50,6 +50,9 @@ struct Region { gainToEffect.reserve(5); // sufficient room for main and fx1-4 gainToEffect.push_back(1.0); // contribute 100% into the main bus + + // Default amplitude release + amplitudeEG.release = Default::ampegRelease; } Region(const Region&) = default; ~Region() = default; From 51459c8f2d2981916803b3040548ff05b1c7b08c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 23 Sep 2020 13:34:01 +0200 Subject: [PATCH 294/445] Updated/added tests --- tests/RegionT.cpp | 2 +- tests/SynthT.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index c7896667..cc2714a0 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1045,7 +1045,7 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.amplitudeEG.decay == 0.0f); REQUIRE(region.amplitudeEG.delay == 0.0f); REQUIRE(region.amplitudeEG.hold == 0.0f); - REQUIRE(region.amplitudeEG.release == 0.0f); + REQUIRE(region.amplitudeEG.release == 0.001f); REQUIRE(region.amplitudeEG.start == 0.0f); REQUIRE(region.amplitudeEG.sustain == 100.0f); REQUIRE(region.amplitudeEG.depth == 0); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 49fccde2..b7a7d1af 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1369,3 +1369,14 @@ TEST_CASE("[Synth] Initial values of CC") REQUIRE(synth.getHdccInit(111) == Approx(0.1234f)); REQUIRE(synth.getHdccInit(112) == Approx(77.0f / 127)); } + +TEST_CASE("[Synth] Default ampeg_release") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path() / "default_release.sfz", R"( + sample=*sine + )"); + + REQUIRE(synth.getRegionView(0)->amplitudeEG.release > 0.0005f); +} From f3fbc1e3277a86538975f69b7b7c0389a9134139 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 14:02:29 +0200 Subject: [PATCH 295/445] Fix the unison osc broken after alignment work --- src/sfizz/Voice.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index ae49816a..bc952482 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -693,8 +693,9 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept else if (oscillatorMode <= 0 && oscillatorMulti >= 3) { // unison oscillator auto tempSpan = resources.bufferPool.getBuffer(numFrames); - auto temp2Span = resources.bufferPool.getBuffer(numFrames); - if (!tempSpan || !temp2Span) + auto tempLeftSpan = resources.bufferPool.getBuffer(numFrames); + auto tempRightSpan = resources.bufferPool.getBuffer(numFrames); + if (!tempSpan || !tempLeftSpan || !tempRightSpan) return; const float* detuneMod = resources.modMatrix.getModulation(oscillatorDetuneTarget); @@ -708,11 +709,18 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept applyGain1(waveDetuneRatio[u], *detuneSpan); } osc.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), numFrames); - multiplyAdd1(waveLeftGain[u], *tempSpan, *temp2Span); - copy(*temp2Span, leftSpan); - multiplyAdd1(waveRightGain[u], *tempSpan, *temp2Span); - copy(*temp2Span, rightSpan); + if (u == 0) { + applyGain1(waveLeftGain[u], *tempSpan, *tempLeftSpan); + applyGain1(waveRightGain[u], *tempSpan, *tempRightSpan); + } + else { + multiplyAdd1(waveLeftGain[u], *tempSpan, *tempLeftSpan); + multiplyAdd1(waveRightGain[u], *tempSpan, *tempRightSpan); + } } + + copy(*tempLeftSpan, leftSpan); + copy(*tempRightSpan, rightSpan); } else { // modulated oscillator From 08dc206ad1de56f2591d3624183e3aca1c99da17 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 17:52:00 +0200 Subject: [PATCH 296/445] Clear the voice span before processing each voice --- src/sfizz/Synth.cpp | 1 - src/sfizz/Voice.cpp | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e63803c1..78dd7336 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -794,7 +794,6 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept activeVoices = 0; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; - tempSpan->fill(0.0f); tempMixSpan->fill(0.0f); resources.filePool.cleanupPromises(); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index bc952482..d165d426 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -661,8 +661,6 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept } else { const auto numFrames = buffer.getNumFrames(); - buffer.fill(0.0f); - auto frequencies = resources.bufferPool.getBuffer(numFrames); if (!frequencies) return; From a1a68e115c00ff598bdbba58a7411d2c16954118 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 22:16:26 +0200 Subject: [PATCH 297/445] Move EG v1 opcode processing in separate routine --- src/sfizz/Region.cpp | 211 +++++++++++++++++++++++++------------------ src/sfizz/Region.h | 10 ++ 2 files changed, 135 insertions(+), 86 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index a98dac2a..a95734b6 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1164,111 +1164,26 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) // Amplitude Envelope case hash("ampeg_attack"): - setValueFromOpcode(opcode, amplitudeEG.attack, Default::egTimeRange); - break; case hash("ampeg_decay"): - setValueFromOpcode(opcode, amplitudeEG.decay, Default::egTimeRange); - break; case hash("ampeg_delay"): - setValueFromOpcode(opcode, amplitudeEG.delay, Default::egTimeRange); - break; case hash("ampeg_hold"): - setValueFromOpcode(opcode, amplitudeEG.hold, Default::egTimeRange); - break; case hash("ampeg_release"): - setValueFromOpcode(opcode, amplitudeEG.release, Default::egTimeRange); - break; case hash("ampeg_start"): - setValueFromOpcode(opcode, amplitudeEG.start, Default::egPercentRange); - break; case hash("ampeg_sustain"): - setValueFromOpcode(opcode, amplitudeEG.sustain, Default::egPercentRange); - break; case hash("ampeg_vel&attack"): - if (opcode.parameters.front() != 2) - return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2attack, Default::egOnCCTimeRange); - break; case hash("ampeg_vel&decay"): - if (opcode.parameters.front() != 2) - return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2decay, Default::egOnCCTimeRange); - break; case hash("ampeg_vel&delay"): - if (opcode.parameters.front() != 2) - return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2delay, Default::egOnCCTimeRange); - break; case hash("ampeg_vel&hold"): - if (opcode.parameters.front() != 2) - return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2hold, Default::egOnCCTimeRange); - break; case hash("ampeg_vel&release"): - if (opcode.parameters.front() != 2) - return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2release, Default::egOnCCTimeRange); - break; case hash("ampeg_vel&sustain"): - if (opcode.parameters.front() != 2) - return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2sustain, Default::egOnCCPercentRange); - break; case hash("ampeg_attack_oncc&"): // also ampeg_attackcc& - if (opcode.parameters.back() >= config::numCCs) - return false; - - if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccAttack[opcode.parameters.back()] = *value; - - break; case hash("ampeg_decay_oncc&"): // also ampeg_decaycc& - if (opcode.parameters.back() >= config::numCCs) - return false; - - if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccDecay[opcode.parameters.back()] = *value; - - break; case hash("ampeg_delay_oncc&"): // also ampeg_delaycc& - if (opcode.parameters.back() >= config::numCCs) - return false; - - if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccDelay[opcode.parameters.back()] = *value; - - break; case hash("ampeg_hold_oncc&"): // also ampeg_holdcc& - if (opcode.parameters.back() >= config::numCCs) - return false; - - if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccHold[opcode.parameters.back()] = *value; - - break; case hash("ampeg_release_oncc&"): // also ampeg_releasecc& - if (opcode.parameters.back() >= config::numCCs) - return false; - - if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccRelease[opcode.parameters.back()] = *value; - - break; case hash("ampeg_start_oncc&"): // also ampeg_startcc& - if (opcode.parameters.back() >= config::numCCs) - return false; - - if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) - amplitudeEG.ccStart[opcode.parameters.back()] = *value; - - break; case hash("ampeg_sustain_oncc&"): // also ampeg_sustaincc& - if (opcode.parameters.back() >= config::numCCs) - return false; - - if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) - amplitudeEG.ccSustain[opcode.parameters.back()] = *value; - + parseEGopcode(opcode, amplitudeEG); break; // Flex envelopes @@ -1369,6 +1284,130 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return true; } +bool sfz::Region::parseEGopcode(const Opcode& opcode, EGDescription& eg) +{ + #define case_any_eg(param) \ + case hash("ampeg_" param): \ + case hash("pitcheg_" param): \ + case hash("fileg_" param) \ + + switch (opcode.lettersOnlyHash) { + case_any_eg("attack"): + setValueFromOpcode(opcode, eg.attack, Default::egTimeRange); + break; + case_any_eg("decay"): + setValueFromOpcode(opcode, eg.decay, Default::egTimeRange); + break; + case_any_eg("delay"): + setValueFromOpcode(opcode, eg.delay, Default::egTimeRange); + break; + case_any_eg("hold"): + setValueFromOpcode(opcode, eg.hold, Default::egTimeRange); + break; + case_any_eg("release"): + setValueFromOpcode(opcode, eg.release, Default::egTimeRange); + break; + case_any_eg("start"): + setValueFromOpcode(opcode, eg.start, Default::egPercentRange); + break; + case_any_eg("sustain"): + setValueFromOpcode(opcode, eg.sustain, Default::egPercentRange); + break; + case_any_eg("vel&attack"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + setValueFromOpcode(opcode, eg.vel2attack, Default::egOnCCTimeRange); + break; + case_any_eg("vel&decay"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + setValueFromOpcode(opcode, eg.vel2decay, Default::egOnCCTimeRange); + break; + case_any_eg("vel&delay"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + setValueFromOpcode(opcode, eg.vel2delay, Default::egOnCCTimeRange); + break; + case_any_eg("vel&hold"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + setValueFromOpcode(opcode, eg.vel2hold, Default::egOnCCTimeRange); + break; + case_any_eg("vel&release"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + setValueFromOpcode(opcode, eg.vel2release, Default::egOnCCTimeRange); + break; + case_any_eg("vel&sustain"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + setValueFromOpcode(opcode, eg.vel2sustain, Default::egOnCCPercentRange); + break; + case_any_eg("attack_oncc&"): // also attackcc& + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + eg.ccAttack[opcode.parameters.back()] = *value; + + break; + case_any_eg("decay_oncc&"): // also decaycc& + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + eg.ccDecay[opcode.parameters.back()] = *value; + + break; + case_any_eg("delay_oncc&"): // also delaycc& + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + eg.ccDelay[opcode.parameters.back()] = *value; + + break; + case_any_eg("hold_oncc&"): // also holdcc& + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + eg.ccHold[opcode.parameters.back()] = *value; + + break; + case_any_eg("release_oncc&"): // also releasecc& + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + eg.ccRelease[opcode.parameters.back()] = *value; + + break; + case_any_eg("start_oncc&"): // also startcc& + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) + eg.ccStart[opcode.parameters.back()] = *value; + + break; + case_any_eg("sustain_oncc&"): // also sustaincc& + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) + eg.ccSustain[opcode.parameters.back()] = *value; + + break; + default: + return false; + } + + return true; + + #undef case_any_eg +} + bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, const ModKey& target) { if (!opcode.isAnyCcN()) diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index d11e87a9..59439b3d 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -256,6 +256,16 @@ struct Region { * @return false */ bool parseOpcode(const Opcode& opcode); + /** + * @brief Parse a opcode which is specific to a particular SFZv1 EG: + * ampeg, pitcheg, fileg. + * + * @param opcode + * @param eg + * @return true if the opcode was properly read and stored. + * @return false + */ + bool parseEGopcode(const Opcode& opcode, EGDescription& eg); /** * @brief Process a generic CC opcode, and fill the modulation parameters. * From 23b7f507600d47ae59e36aa382c791049cfe9d1b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 22:36:44 +0200 Subject: [PATCH 298/445] Copy and move assignments for EGDescription --- src/sfizz/CCMap.h | 4 +++- src/sfizz/EGDescription.h | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sfizz/CCMap.h b/src/sfizz/CCMap.h index dce9813f..4fcc13f8 100644 --- a/src/sfizz/CCMap.h +++ b/src/sfizz/CCMap.h @@ -38,6 +38,8 @@ public: CCMap(CCMap&&) = default; CCMap(const CCMap&) = default; ~CCMap() = default; + CCMap& operator=(CCMap&&) = default; + CCMap& operator=(const CCMap&) = default; /** * @brief Returns the held object at the index, or a default value if not present @@ -105,7 +107,7 @@ private: // typename std::vector>::iterator begin() { return container.begin(); } // typename std::vector>::iterator end() { return container.end(); } - const ValueType defaultValue; + ValueType defaultValue; std::vector> container; LEAK_DETECTOR(CCMap); }; diff --git a/src/sfizz/EGDescription.h b/src/sfizz/EGDescription.h index 683b4d51..5cfe0d94 100644 --- a/src/sfizz/EGDescription.h +++ b/src/sfizz/EGDescription.h @@ -63,6 +63,8 @@ struct EGDescription { EGDescription(const EGDescription&) = default; EGDescription(EGDescription&&) = default; ~EGDescription() = default; + EGDescription& operator=(const EGDescription&) = default; + EGDescription& operator=(EGDescription&&) = default; float attack { Default::attack }; float decay { Default::decay }; From 0f2bb804347e83aded33c3aa531084b77064387a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 22:41:12 +0200 Subject: [PATCH 299/445] Add pitcheg and fileg opcodes, and modulation key --- src/sfizz/Region.cpp | 65 ++++++++++++++++++++++++++++++++ src/sfizz/Region.h | 16 ++++++-- src/sfizz/Synth.cpp | 32 +++++++++------- src/sfizz/modulations/ModId.cpp | 4 ++ src/sfizz/modulations/ModId.h | 2 + src/sfizz/modulations/ModKey.cpp | 4 ++ 6 files changed, 106 insertions(+), 17 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index a95734b6..7ca3e928 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1186,6 +1186,58 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) parseEGopcode(opcode, amplitudeEG); break; + case hash("pitcheg_attack"): + case hash("pitcheg_decay"): + case hash("pitcheg_delay"): + case hash("pitcheg_hold"): + case hash("pitcheg_release"): + case hash("pitcheg_start"): + case hash("pitcheg_sustain"): + case hash("pitcheg_vel&attack"): + case hash("pitcheg_vel&decay"): + case hash("pitcheg_vel&delay"): + case hash("pitcheg_vel&hold"): + case hash("pitcheg_vel&release"): + case hash("pitcheg_vel&sustain"): + case hash("pitcheg_attack_oncc&"): // also pitcheg_attackcc& + case hash("pitcheg_decay_oncc&"): // also pitcheg_decaycc& + case hash("pitcheg_delay_oncc&"): // also pitcheg_delaycc& + case hash("pitcheg_hold_oncc&"): // also pitcheg_holdcc& + case hash("pitcheg_release_oncc&"): // also pitcheg_releasecc& + case hash("pitcheg_start_oncc&"): // also pitcheg_startcc& + case hash("pitcheg_sustain_oncc&"): // also pitcheg_sustaincc& + if (parseEGopcode(opcode, pitchEG)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::PitchEG, id), + ModKey::createNXYZ(ModId::Pitch, id)); + break; + + case hash("fileg_attack"): + case hash("fileg_decay"): + case hash("fileg_delay"): + case hash("fileg_hold"): + case hash("fileg_release"): + case hash("fileg_start"): + case hash("fileg_sustain"): + case hash("fileg_vel&attack"): + case hash("fileg_vel&decay"): + case hash("fileg_vel&delay"): + case hash("fileg_vel&hold"): + case hash("fileg_vel&release"): + case hash("fileg_vel&sustain"): + case hash("fileg_attack_oncc&"): // also fileg_attackcc& + case hash("fileg_decay_oncc&"): // also fileg_decaycc& + case hash("fileg_delay_oncc&"): // also fileg_delaycc& + case hash("fileg_hold_oncc&"): // also fileg_holdcc& + case hash("fileg_release_oncc&"): // also fileg_releasecc& + case hash("fileg_start_oncc&"): // also fileg_startcc& + case hash("fileg_sustain_oncc&"): // also fileg_sustaincc& + if (parseEGopcode(opcode, filterEG)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::FilEG, id), + ModKey::createNXYZ(ModId::FilCutoff, id)); + break; + // Flex envelopes case hash("eg&_dynamic"): { @@ -1408,6 +1460,19 @@ bool sfz::Region::parseEGopcode(const Opcode& opcode, EGDescription& eg) #undef case_any_eg } +bool sfz::Region::parseEGopcode(const Opcode& opcode, absl::optional& eg) +{ + bool create = eg == absl::nullopt; + if (create) + eg = EGDescription(); + + bool parsed = parseEGopcode(opcode, *eg); + if (!parsed && create) + eg = absl::nullopt; + + return parsed; +} + bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, const ModKey& target) { if (!opcode.isAnyCcN()) diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 59439b3d..6bb15849 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -266,6 +266,16 @@ struct Region { * @return false */ bool parseEGopcode(const Opcode& opcode, EGDescription& eg); + /** + * @brief Parse a opcode which is specific to a particular SFZv1 EG: + * ampeg, pitcheg, fileg. + * + * @param opcode + * @param eg + * @return true if the opcode was properly read and stored. + * @return false + */ + bool parseEGopcode(const Opcode& opcode, absl::optional& eg); /** * @brief Process a generic CC opcode, and fill the modulation parameters. * @@ -410,8 +420,8 @@ struct Region { // Envelopes EGDescription amplitudeEG; - EGDescription pitchEG; - EGDescription filterEG; + absl::optional pitchEG; + absl::optional filterEG; // Envelopes std::vector flexEGs; @@ -431,7 +441,7 @@ struct Region { struct Connection { ModKey source; ModKey target; - float sourceDepth = 1.0f; + float sourceDepth = 0.0f; }; std::vector connections; Connection& getOrCreateConnection(const ModKey& source, const ModKey& target); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c08dde96..c58340f8 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1702,20 +1702,24 @@ void sfz::Synth::updateUsedCCsFromRegion(std::bitset& usedC updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccHold); updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccStart); updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccSustain); - updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccAttack); - updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccRelease); - updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccDecay); - updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccDelay); - updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccHold); - updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccStart); - updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccSustain); - updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccAttack); - updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccRelease); - updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccDecay); - updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccDelay); - updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccHold); - updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccStart); - updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccSustain); + if (region.pitchEG) { + updateUsedCCsFromCCMap(usedCCs, region.pitchEG->ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG->ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG->ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG->ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG->ccHold); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG->ccStart); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG->ccSustain); + } + if (region.filterEG) { + updateUsedCCsFromCCMap(usedCCs, region.filterEG->ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.filterEG->ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.filterEG->ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.filterEG->ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.filterEG->ccHold); + updateUsedCCsFromCCMap(usedCCs, region.filterEG->ccStart); + updateUsedCCsFromCCMap(usedCCs, region.filterEG->ccSustain); + } updateUsedCCsFromCCMap(usedCCs, region.ccConditions); updateUsedCCsFromCCMap(usedCCs, region.ccTriggers); updateUsedCCsFromCCMap(usedCCs, region.crossfadeCCInRange); diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index f294a0bc..427d7511 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -30,6 +30,10 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice; case ModId::LFO: return kModIsPerVoice; + case ModId::PitchEG: + return kModIsPerVoice; + case ModId::FilEG: + return kModIsPerVoice; // targets case ModId::Amplitude: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index daf7f72f..ccd25b98 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -23,6 +23,8 @@ enum class ModId : int { Controller = _SourcesStart, Envelope, LFO, + PitchEG, + FilEG, _SourcesEnd, diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index efb6c8a0..80c3accb 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -74,6 +74,10 @@ std::string ModKey::toString() const return absl::StrCat("EG ", 1 + params_.N, " {", region_.number(), "}"); case ModId::LFO: return absl::StrCat("LFO ", 1 + params_.N, " {", region_.number(), "}"); + case ModId::PitchEG: + return absl::StrCat("PitchEG {", region_.number(), "}"); + case ModId::FilEG: + return absl::StrCat("FilterEG {", region_.number(), "}"); case ModId::Amplitude: return absl::StrCat("Amplitude {", region_.number(), "}"); From 2f12206c4c77ac98f12ea41445372e527a5f7e9a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 22:58:53 +0200 Subject: [PATCH 300/445] Rename `egEnvelope` to `egAmplitude` --- src/sfizz/Voice.cpp | 20 ++++++++++---------- src/sfizz/Voice.h | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index d165d426..81f7c158 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -130,7 +130,7 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event bendStepFactor = centsFactor(region->bendStep); bendSmoother.setSmoothing(region->bendSmooth, sampleRate); bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); - egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate); + egAmplitude.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate); resources.modMatrix.initVoice(id, region->getId(), delay); saveModulationTargets(region); @@ -152,10 +152,10 @@ void sfz::Voice::release(int delay) noexcept if (state != State::playing) return; - if (egEnvelope.getRemainingDelay() > delay) { + if (egAmplitude.getRemainingDelay() > delay) { switchState(State::cleanMeUp); } else { - egEnvelope.startRelease(delay); + egAmplitude.startRelease(delay); } resources.modMatrix.releaseVoice(id, region->getId(), delay); @@ -164,9 +164,9 @@ void sfz::Voice::release(int delay) noexcept void sfz::Voice::off(int delay) noexcept { if (region->offMode == SfzOffMode::fast) { - egEnvelope.setReleaseTime( Default::offTime ); + egAmplitude.setReleaseTime( Default::offTime ); } else if (region->offMode == SfzOffMode::time) { - egEnvelope.setReleaseTime(region->offTime); + egAmplitude.setReleaseTime(region->offTime); } release(delay); @@ -286,7 +286,7 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept panStageMono(buffer); } - if (!egEnvelope.isSmoothing()) + if (!egAmplitude.isSmoothing()) switchState(State::cleanMeUp); powerFollower.process(buffer); @@ -368,7 +368,7 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept ModMatrix& mm = resources.modMatrix; // AmpEG envelope - egEnvelope.getBlock(modulationSpan); + egAmplitude.getBlock(modulationSpan); // Amplitude envelope applyGain1(baseGain, modulationSpan); @@ -572,8 +572,8 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept << " for sample " << region->sampleId); } #endif - egEnvelope.setReleaseTime(0.0f); - egEnvelope.startRelease(i); + egAmplitude.setReleaseTime(0.0f); + egAmplitude.startRelease(i); fill(indices->subspan(i), sampleEnd); fill(coeffs->subspan(i), 1.0f); break; @@ -853,7 +853,7 @@ float sfz::Voice::getAveragePower() const noexcept bool sfz::Voice::releasedOrFree() const noexcept { - return state != State::playing || egEnvelope.isReleased(); + return state != State::playing || egAmplitude.isReleased(); } uint32_t sfz::Voice::getSourcePosition() const noexcept diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 348022f6..02062752 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -461,7 +461,7 @@ private: std::vector> lfos; std::vector> flexEGs; - ADSREnvelope egEnvelope; + ADSREnvelope egAmplitude; float bendStepFactor { centsFactor(1) }; WavetableOscillator waveOscillators[config::oscillatorsPerVoice]; From b99b40b131fa986c4dbd8edb3c7de55cb74e9afd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 23:32:47 +0200 Subject: [PATCH 301/445] Implement fileg and pitcheg in the matrix --- dpf.mk | 1 + src/CMakeLists.txt | 2 + src/sfizz/Defaults.h | 2 + src/sfizz/Region.cpp | 16 +++ src/sfizz/Synth.cpp | 14 +++ src/sfizz/Synth.h | 5 + src/sfizz/Voice.cpp | 16 +++ src/sfizz/Voice.h | 28 +++++ .../modulations/sources/ADSREnvelope.cpp | 117 ++++++++++++++++++ src/sfizz/modulations/sources/ADSREnvelope.h | 24 ++++ 10 files changed, 225 insertions(+) create mode 100644 src/sfizz/modulations/sources/ADSREnvelope.cpp create mode 100644 src/sfizz/modulations/sources/ADSREnvelope.h diff --git a/dpf.mk b/dpf.mk index 1c8159fa..71d4555f 100644 --- a/dpf.mk +++ b/dpf.mk @@ -65,6 +65,7 @@ SFIZZ_SOURCES = \ src/sfizz/modulations/ModKey.cpp \ src/sfizz/modulations/ModKeyHash.cpp \ src/sfizz/modulations/ModMatrix.cpp \ + src/sfizz/modulations/sources/ADSREnvelope.cpp \ src/sfizz/modulations/sources/Controller.cpp \ src/sfizz/modulations/sources/FlexEGDescription.cpp \ src/sfizz/modulations/sources/FlexEnvelope.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f65ea3c..11d48265 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ set (SFIZZ_HEADERS sfizz/modulations/ModKeyHash.h sfizz/modulations/ModMatrix.h sfizz/modulations/ModGenerator.h + sfizz/modulations/sources/ADSREnvelope.h sfizz/modulations/sources/Controller.h sfizz/modulations/sources/FlexEnvelope.h sfizz/modulations/sources/LFO.h @@ -151,6 +152,7 @@ set (SFIZZ_SOURCES sfizz/modulations/ModMatrix.cpp sfizz/modulations/sources/Controller.cpp sfizz/modulations/sources/FlexEnvelope.cpp + sfizz/modulations/sources/ADSREnvelope.cpp sfizz/modulations/sources/LFO.cpp sfizz/utility/SpinMutex.cpp sfizz/effects/Nothing.cpp diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 27b7c240..1550e749 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -250,6 +250,8 @@ namespace Default constexpr Range egDepthRange { -12000, 12000 }; constexpr Range egOnCCTimeRange { -100.0, 100.0 }; constexpr Range egOnCCPercentRange { -100.0, 100.0 }; + constexpr Range pitchEgDepthRange { -12000.0, 12000.0 }; + constexpr Range filterEgDepthRange { -12000.0, 12000.0 }; // Flex envelope generators constexpr int numFlexEGs { 4 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 7ca3e928..967e92d9 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1238,6 +1238,22 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) ModKey::createNXYZ(ModId::FilCutoff, id)); break; + case hash("pitcheg_depth"): + if (auto value = readOpcode(opcode.value, Default::pitchEgDepthRange)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::PitchEG, id), + ModKey::createNXYZ(ModId::Pitch, id)).sourceDepth = *value; + break; + case hash("fileg_depth"): + if (auto value = readOpcode(opcode.value, Default::filterEgDepthRange)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::FilEG, id), + ModKey::createNXYZ(ModId::FilCutoff, id)).sourceDepth = *value; + break; + + // TODO(jpc): pitcheg_vel2depth + // TODO(jpc): fileg_vel2depth + // Flex envelopes case hash("eg&_dynamic"): { diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c58340f8..c928f48d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -19,6 +19,7 @@ #include "modulations/sources/Controller.h" #include "modulations/sources/LFO.h" #include "modulations/sources/FlexEnvelope.h" +#include "modulations/sources/ADSREnvelope.h" #include "utility/XmlHelpers.h" #include "pugixml.hpp" #include "absl/algorithm/container.h" @@ -49,6 +50,7 @@ sfz::Synth::Synth(int numVoices) genController.reset(new ControllerSource(resources)); genLFO.reset(new LFOSource(*this)); genFlexEnvelope.reset(new FlexEnvelopeSource(*this)); + genADSREnvelope.reset(new ADSREnvelopeSource(*this)); } sfz::Synth::~Synth() @@ -489,6 +491,8 @@ void sfz::Synth::finalizeSfzLoad() size_t maxEQs { 0 }; size_t maxLFOs { 0 }; size_t maxFlexEGs { 0 }; + bool havePitchEG { false }; + bool haveFilterEG { false }; FlexEGs::clearUnusedCurves(); @@ -613,6 +617,8 @@ void sfz::Synth::finalizeSfzLoad() maxEQs = max(maxEQs, region->equalizers.size()); maxLFOs = max(maxLFOs, region->lfos.size()); maxFlexEGs = max(maxFlexEGs, region->flexEGs.size()); + havePitchEG = havePitchEG || region->pitchEG != absl::nullopt; + haveFilterEG = haveFilterEG || region->filterEG != absl::nullopt; ++currentRegionIndex; } @@ -624,6 +630,8 @@ void sfz::Synth::finalizeSfzLoad() settingsPerVoice.maxEQs = maxEQs; settingsPerVoice.maxLFOs = maxLFOs; settingsPerVoice.maxFlexEGs = maxFlexEGs; + settingsPerVoice.havePitchEG = havePitchEG; + settingsPerVoice.haveFilterEG = haveFilterEG; applySettingsPerVoice(); @@ -1499,6 +1507,8 @@ void sfz::Synth::applySettingsPerVoice() voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); voice->setMaxLFOsPerVoice(settingsPerVoice.maxLFOs); voice->setMaxFlexEGsPerVoice(settingsPerVoice.maxFlexEGs); + voice->setPitchEGEnabledPerVoice(settingsPerVoice.havePitchEG); + voice->setFilterEGEnabledPerVoice(settingsPerVoice.haveFilterEG); } } @@ -1520,6 +1530,10 @@ void sfz::Synth::setupModMatrix() case ModId::Envelope: gen = genFlexEnvelope.get(); break; + case ModId::PitchEG: + case ModId::FilEG: + gen = genADSREnvelope.get(); + break; default: DBG("[sfizz] Have unknown type of source generator"); break; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index c308666e..880726cf 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -29,6 +29,7 @@ namespace sfz { class ControllerSource; class LFOSource; class FlexEnvelopeSource; +class ADSREnvelopeSource; /** * @brief This class is the core of the sfizz library. In C++ it is the main point @@ -550,6 +551,7 @@ public: */ void disableFreeWheeling() noexcept; + Resources& getResources() noexcept { return resources; } const Resources& getResources() const noexcept { return resources; } /** @@ -922,6 +924,7 @@ private: std::unique_ptr genController; std::unique_ptr genLFO; std::unique_ptr genFlexEnvelope; + std::unique_ptr genADSREnvelope; // Settings per voice struct SettingsPerVoice { @@ -929,6 +932,8 @@ private: size_t maxEQs { 0 }; size_t maxLFOs { 0 }; size_t maxFlexEGs { 0 }; + bool havePitchEG { false }; + bool haveFilterEG { false }; }; SettingsPerVoice settingsPerVoice; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 81f7c158..a60b1af7 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -903,6 +903,22 @@ void sfz::Voice::setMaxFlexEGsPerVoice(size_t numFlexEGs) } } +void sfz::Voice::setPitchEGEnabledPerVoice(bool havePitchEG) +{ + if (havePitchEG) + egPitch.reset(new ADSREnvelope); + else + egPitch.reset(); +} + +void sfz::Voice::setFilterEGEnabledPerVoice(bool haveFilterEG) +{ + if (haveFilterEG) + egFilter.reset(new ADSREnvelope); + else + egFilter.reset(); +} + void sfz::Voice::setupOscillatorUnison() { const int m = region->oscillatorMulti; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 02062752..419c6584 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -296,6 +296,18 @@ public: * @param numFlexEGs */ void setMaxFlexEGsPerVoice(size_t numFlexEGs); + /** + * @brief Set whether SFZv1 pitch EG is enabled on this voice + * + * @param havePitchEG + */ + void setPitchEGEnabledPerVoice(bool havePitchEG); + /** + * @brief Set whether SFZv1 filter EG is enabled on this voice + * + * @param haveFilterEG + */ + void setFilterEGEnabledPerVoice(bool haveFilterEG); /** * @brief Release the voice after a given delay * @@ -324,6 +336,20 @@ public: Duration getLastFilterDuration() const noexcept { return filterDuration; } Duration getLastPanningDuration() const noexcept { return panningDuration; } + /** + * @brief Get the SFZv1 pitch EG, if existing + */ + ADSREnvelope* getPitchEG() { return egPitch.get(); } + /** + * @brief Get the SFZv1 filter EG, if existing + */ + ADSREnvelope* getFilterEG() { return egFilter.get(); } + + /** + * @brief Get the trigger event + */ + const TriggerEvent& getTriggerEvent() { return triggerEvent; } + private: /** * @brief Fill a span with data from a file source. This is the first step @@ -462,6 +488,8 @@ private: std::vector> flexEGs; ADSREnvelope egAmplitude; + std::unique_ptr> egPitch; + std::unique_ptr> egFilter; float bendStepFactor { centsFactor(1) }; WavetableOscillator waveOscillators[config::oscillatorsPerVoice]; diff --git a/src/sfizz/modulations/sources/ADSREnvelope.cpp b/src/sfizz/modulations/sources/ADSREnvelope.cpp new file mode 100644 index 00000000..7128b216 --- /dev/null +++ b/src/sfizz/modulations/sources/ADSREnvelope.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ADSREnvelope.h" +#include "../../ADSREnvelope.h" +#include "../../Synth.h" +#include "../../Voice.h" +#include "../../Config.h" +#include "../../Debug.h" + +// TODO(jpc): also matrix the ampeg + +namespace sfz { + +ADSREnvelopeSource::ADSREnvelopeSource(Synth &synth) + : synth_(&synth) +{ +} + +void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + Synth& synth = *synth_; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + const Region* region = voice->getRegion(); + ADSREnvelope* eg = nullptr; + const EGDescription* desc = nullptr; + + switch (sourceKey.id()) { + case ModId::PitchEG: + eg = voice->getPitchEG(); + ASSERT(eg); + desc = &*region->pitchEG; + break; + case ModId::FilEG: + eg = voice->getFilterEG(); + ASSERT(eg); + desc = &*region->filterEG; + break; + default: + ASSERTFALSE; + return; + } + + Resources& resources = synth.getResources(); + const TriggerEvent& triggerEvent = voice->getTriggerEvent(); + const float sampleRate = voice->getSampleRate(); + eg->reset(*desc, *region, resources.midiState, delay, triggerEvent.value, sampleRate); +} + +void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + Synth& synth = *synth_; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + ADSREnvelope* eg = nullptr; + + switch (sourceKey.id()) { + case ModId::PitchEG: + eg = voice->getPitchEG(); + ASSERT(eg); + break; + case ModId::FilEG: + eg = voice->getFilterEG(); + ASSERT(eg); + break; + default: + ASSERTFALSE; + return; + } + + eg->startRelease(delay); +} + +void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + Synth& synth = *synth_; + + Voice* voice = synth.getVoiceById(voiceId); + if (!voice) { + ASSERTFALSE; + return; + } + + ADSREnvelope* eg = nullptr; + + switch (sourceKey.id()) { + case ModId::PitchEG: + eg = voice->getPitchEG(); + ASSERT(eg); + break; + case ModId::FilEG: + eg = voice->getFilterEG(); + ASSERT(eg); + break; + default: + ASSERTFALSE; + return; + } + + eg->getBlock(buffer); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/ADSREnvelope.h b/src/sfizz/modulations/sources/ADSREnvelope.h new file mode 100644 index 00000000..b2644df9 --- /dev/null +++ b/src/sfizz/modulations/sources/ADSREnvelope.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../ModGenerator.h" + +namespace sfz { +class Synth; + +class ADSREnvelopeSource : public ModGenerator { +public: + explicit ADSREnvelopeSource(Synth &synth); + void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + Synth* synth_ = nullptr; +}; + +} // namespace sfz From 4a998cf6e70422e4159a1fff9786851e282aa95c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 23 Sep 2020 23:54:31 +0200 Subject: [PATCH 302/445] Forgot to update source depths after changing default to 0 --- src/sfizz/Synth.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c928f48d..7f6b6d10 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -154,10 +154,10 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) constexpr unsigned defaultSmoothness = 10; lastRegion->getOrCreateConnection( ModKey::createCC(7, 4, defaultSmoothness, 100, 0), - ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)); + ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 1.0f; lastRegion->getOrCreateConnection( ModKey::createCC(10, 1, defaultSmoothness, 100, 0), - ModKey::createNXYZ(ModId::Pan, lastRegion->id)); + ModKey::createNXYZ(ModId::Pan, lastRegion->id)).sourceDepth = 1.0f; // auto parseOpcodes = [&](const std::vector& opcodes) { From 897477e1401dce549670192b77a0c15a1aef5ce8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 24 Sep 2020 15:12:58 +0200 Subject: [PATCH 303/445] Just a function renamed --- src/sfizz/Region.cpp | 12 ++++++------ src/sfizz/Region.h | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 967e92d9..a16c91a5 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1183,7 +1183,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("ampeg_release_oncc&"): // also ampeg_releasecc& case hash("ampeg_start_oncc&"): // also ampeg_startcc& case hash("ampeg_sustain_oncc&"): // also ampeg_sustaincc& - parseEGopcode(opcode, amplitudeEG); + parseEGOpcode(opcode, amplitudeEG); break; case hash("pitcheg_attack"): @@ -1206,7 +1206,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("pitcheg_release_oncc&"): // also pitcheg_releasecc& case hash("pitcheg_start_oncc&"): // also pitcheg_startcc& case hash("pitcheg_sustain_oncc&"): // also pitcheg_sustaincc& - if (parseEGopcode(opcode, pitchEG)) + if (parseEGOpcode(opcode, pitchEG)) getOrCreateConnection( ModKey::createNXYZ(ModId::PitchEG, id), ModKey::createNXYZ(ModId::Pitch, id)); @@ -1232,7 +1232,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("fileg_release_oncc&"): // also fileg_releasecc& case hash("fileg_start_oncc&"): // also fileg_startcc& case hash("fileg_sustain_oncc&"): // also fileg_sustaincc& - if (parseEGopcode(opcode, filterEG)) + if (parseEGOpcode(opcode, filterEG)) getOrCreateConnection( ModKey::createNXYZ(ModId::FilEG, id), ModKey::createNXYZ(ModId::FilCutoff, id)); @@ -1352,7 +1352,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) return true; } -bool sfz::Region::parseEGopcode(const Opcode& opcode, EGDescription& eg) +bool sfz::Region::parseEGOpcode(const Opcode& opcode, EGDescription& eg) { #define case_any_eg(param) \ case hash("ampeg_" param): \ @@ -1476,13 +1476,13 @@ bool sfz::Region::parseEGopcode(const Opcode& opcode, EGDescription& eg) #undef case_any_eg } -bool sfz::Region::parseEGopcode(const Opcode& opcode, absl::optional& eg) +bool sfz::Region::parseEGOpcode(const Opcode& opcode, absl::optional& eg) { bool create = eg == absl::nullopt; if (create) eg = EGDescription(); - bool parsed = parseEGopcode(opcode, *eg); + bool parsed = parseEGOpcode(opcode, *eg); if (!parsed && create) eg = absl::nullopt; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 6bb15849..ca55827f 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -265,7 +265,7 @@ struct Region { * @return true if the opcode was properly read and stored. * @return false */ - bool parseEGopcode(const Opcode& opcode, EGDescription& eg); + bool parseEGOpcode(const Opcode& opcode, EGDescription& eg); /** * @brief Parse a opcode which is specific to a particular SFZv1 EG: * ampeg, pitcheg, fileg. @@ -275,7 +275,7 @@ struct Region { * @return true if the opcode was properly read and stored. * @return false */ - bool parseEGopcode(const Opcode& opcode, absl::optional& eg); + bool parseEGOpcode(const Opcode& opcode, absl::optional& eg); /** * @brief Process a generic CC opcode, and fill the modulation parameters. * From 48ddb4ad60db0a8ff7ba29747109d1f28e91f70f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 24 Sep 2020 16:12:52 +0200 Subject: [PATCH 304/445] Add vel2depth --- src/sfizz/Region.cpp | 18 ++++++++++++++++-- src/sfizz/Region.h | 1 + src/sfizz/Synth.cpp | 4 ++-- src/sfizz/modulations/ModMatrix.cpp | 20 +++++++++++++++++--- src/sfizz/modulations/ModMatrix.h | 6 ++++-- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index a16c91a5..7d69d202 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1251,8 +1251,22 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) ModKey::createNXYZ(ModId::FilCutoff, id)).sourceDepth = *value; break; - // TODO(jpc): pitcheg_vel2depth - // TODO(jpc): fileg_vel2depth + case hash("pitcheg_vel&depth"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + if (auto value = readOpcode(opcode.value, Default::pitchEgDepthRange)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::PitchEG, id), + ModKey::createNXYZ(ModId::Pitch, id)).velToDepth = *value; + break; + case hash("fileg_vel&depth"): + if (opcode.parameters.front() != 2) + return false; // Was not vel2... + if (auto value = readOpcode(opcode.value, Default::filterEgDepthRange)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::FilEG, id), + ModKey::createNXYZ(ModId::FilCutoff, id)).velToDepth = *value; + break; // Flex envelopes case hash("eg&_dynamic"): diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index ca55827f..fabe91c4 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -442,6 +442,7 @@ struct Region { ModKey source; ModKey target; float sourceDepth = 0.0f; + float velToDepth = 0.0f; }; std::vector connections; Connection& getOrCreateConnection(const ModKey& source, const ModKey& target); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 7f6b6d10..b191d841 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -820,7 +820,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; - mm.beginVoice(voice->getId(), voice->getRegion()->getId()); + mm.beginVoice(voice->getId(), voice->getRegion()->getId(), voice->getTriggerEvent().value); activeVoices++; renderVoiceToOutputs(*voice, *tempSpan); @@ -1558,7 +1558,7 @@ void sfz::Synth::setupModMatrix() continue; } - if (!mm.connect(source, target, conn.sourceDepth)) { + if (!mm.connect(source, target, conn.sourceDepth, conn.velToDepth)) { DBG("[sfizz] Failed to connect modulation source and target"); ASSERTFALSE; } diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index d7ddc444..ffba3f6a 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -27,6 +27,8 @@ struct ModMatrix::Impl { NumericId currentVoiceId_ {}; NumericId currentRegionId_ {}; + float currentVoiceTriggerValue_ {}; + struct Source { ModKey key; ModGenerator* gen {}; @@ -36,6 +38,7 @@ struct ModMatrix::Impl { struct ConnectionData { float sourceDepth_ {}; + float velToDepth_ {}; }; struct Target { @@ -190,7 +193,7 @@ ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key) const return TargetId(it->second); } -bool ModMatrix::connect(SourceId sourceId, TargetId targetId, float sourceDepth) +bool ModMatrix::connect(SourceId sourceId, TargetId targetId, float sourceDepth, float velToDepth) { Impl& impl = *impl_; unsigned sourceIndex = sourceId.number(); @@ -202,6 +205,7 @@ bool ModMatrix::connect(SourceId sourceId, TargetId targetId, float sourceDepth) Impl::Target& target = impl.targets_[targetIndex]; Impl::ConnectionData& conn = target.connectedSources[sourceIndex]; conn.sourceDepth_ = sourceDepth; + conn.velToDepth_ = velToDepth; return true; } @@ -302,13 +306,15 @@ void ModMatrix::endCycle() impl.numFrames_ = 0; } -void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId) +void ModMatrix::beginVoice(NumericId voiceId, NumericId regionId, float triggerValue) { Impl& impl = *impl_; impl.currentVoiceId_ = voiceId; impl.currentRegionId_ = regionId; + impl.currentVoiceTriggerValue_ = triggerValue; + ASSERT(regionId); const auto idNumber = static_cast(regionId.number()); @@ -345,6 +351,8 @@ void ModMatrix::endVoice() impl.currentVoiceId_ = {}; impl.currentRegionId_ = {}; + + impl.currentVoiceTriggerValue_ = 0.0f; } float* ModMatrix::getModulation(TargetId targetId) @@ -354,6 +362,7 @@ float* ModMatrix::getModulation(TargetId targetId) Impl& impl = *impl_; const NumericId regionId = impl.currentRegionId_; + const float triggerValue = impl.currentVoiceTriggerValue_; const uint32_t targetIndex = targetId.number(); Impl::Target &target = impl.targets_[targetIndex]; const int targetFlags = target.key.flags(); @@ -381,7 +390,6 @@ float* ModMatrix::getModulation(TargetId targetId) // then add or multiply, depending on target flags while (sourcesPos != sourcesEnd) { Impl::Source &source = impl.sources_[sourcesPos->first]; - const float sourceDepth = sourcesPos->second.sourceDepth_; const int sourceFlags = source.key.flags(); // only accept per-voice sources of the same region @@ -398,6 +406,12 @@ float* ModMatrix::getModulation(TargetId targetId) source.bufferReady = true; } + float sourceDepth = sourcesPos->second.sourceDepth_; + if (sourceFlags & kModIsPerVoice) { + const float velToDepth = sourcesPos->second.velToDepth_; + sourceDepth += triggerValue * velToDepth; + } + if (isFirstSource) { if (sourceDepth != 1) { for (uint32_t i = 0; i < numFrames; ++i) diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index 0f0fb471..0a01dd5b 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -92,9 +92,10 @@ public: * @param sourceId source of the connection * @param targetId target of the connection * @param sourceDepth amount which multiplies the source output + * @param velToDepth amount which full velocity adds to the source depth * @return true if the connection was successfully made, otherwise false */ - bool connect(SourceId sourceId, TargetId targetId, float sourceDepth); + bool connect(SourceId sourceId, TargetId targetId, float sourceDepth, float velToDepth = 0.0f); /** * @brief Reinitialize modulation sources overall. @@ -134,8 +135,9 @@ public: * * @param voiceId the identifier of the current voice * @param regionId the identifier of the region of the current voice + * @param triggerValue the velocity of the current voice */ - void beginVoice(NumericId voiceId, NumericId regionId); + void beginVoice(NumericId voiceId, NumericId regionId, float triggerValue); /** * @brief End modulation processing for a given voice. From 6895d44cec92feb472b6009411e8d9c9534df93d Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 25 Sep 2020 04:08:57 +0200 Subject: [PATCH 305/445] parser: make opcode values stop at the '<' character --- src/sfizz/parser/Parser.cpp | 18 ++++++++++-------- tests/ParsingT.cpp | 7 ++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index 071bc1e2..9ed460c2 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -297,18 +297,20 @@ void Parser::processOpcode() for (size_t valueSize = valueRaw.size(); endPosition < valueSize;) { size_t i = endPosition + 1; - if (isSpaceChar(valueRaw[endPosition])) { - // check if the rest of the string is to consume or not - bool stop = false; + bool stop = false; + // if a "<" character is next, a header follows + if (valueRaw[endPosition] == '<') + stop = true; + // if space, check if the rest of the string is to consume or not + else if (isSpaceChar(valueRaw[endPosition])) { // consume space characters following while (i < valueSize && isSpaceChar(valueRaw[i])) ++i; - // if there aren't non-space characters following, do not extract if (i == valueSize) stop = true; - // if a "=" or "<" character is next, a header or a directive follows + // if a "<" or "#" character is next, a header or a directive follows else if (valueRaw[i] == '<' || valueRaw[i] == '#') stop = true; // if sequence of identifier chars and then "=", an opcode follows @@ -319,11 +321,11 @@ void Parser::processOpcode() if (i < valueSize && valueRaw[i] == '=') stop = true; } - - if (stop) - break; } + if (stop) + break; + endPosition = i; } diff --git a/tests/ParsingT.cpp b/tests/ParsingT.cpp index a1e12266..1224b46c 100644 --- a/tests/ParsingT.cpp +++ b/tests/ParsingT.cpp @@ -673,14 +673,15 @@ TEST_CASE("[Parsing] Opcode value special character") R"( sample=Alto-Flute-sus-C#4-PB-loop.wav -sample=foo=bar)"); std::vector> expectedMembers = { {{"sample", "Alto-Flute-sus-C#4-PB-loop.wav"}}, - {{"sample", "foo=bar expectedHeaders = { - "region", "region" + "region", "region", "group" }; std::vector expectedOpcodes; From 766b84cfa52d637e630905969fcaf89fc892ae73 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 25 Sep 2020 17:18:05 +0200 Subject: [PATCH 306/445] Rename extended opcode for consistency --- src/sfizz/Region.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index a98dac2a..b78a38d0 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1057,7 +1057,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("lfo&_resonance&"): LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilResonance, Default::filterResonanceModRange); break; - case hash("lfo&_fil&_gain"): + case hash("lfo&_fil&gain"): LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilGain, Default::filterGainModRange); break; case hash("lfo&_eq&gain"): From 8f12dd1c5e0d249b1637ed37c3f40a16e7b64e8c Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 25 Sep 2020 19:08:53 +0200 Subject: [PATCH 307/445] Same with egN_filXgain --- src/sfizz/Region.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index b78a38d0..23e6be98 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1149,7 +1149,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("eg&_resonance&"): LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilResonance, Default::filterResonanceModRange); break; - case hash("eg&_fil&_gain"): + case hash("eg&_fil&gain"): LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilGain, Default::filterGainModRange); break; case hash("eg&_eq&gain"): From 3a584481c68490eb48ced97b123ccd0ba866425f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 25 Sep 2020 19:09:01 +0200 Subject: [PATCH 308/445] Update tests --- tests/ModulationsT.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 45db5737..05136501 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -146,7 +146,7 @@ TEST_CASE("[Modulations] LFO Filter connections") lfo3_freq=2 lfo3_resonance=3 lfo4_freq=0.5 lfo4_resonance1=4 lfo5_freq=0.5 lfo5_resonance2=5 - lfo6_freq=3 lfo6_fil1_gain=-1 + lfo6_freq=3 lfo6_fil1gain=-1 )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); @@ -170,7 +170,7 @@ TEST_CASE("[Modulations] EG Filter connections") eg3_time1=2 eg3_resonance=3 eg4_time1=0.5 eg4_resonance1=4 eg5_time1=0.5 eg5_resonance2=5 - eg6_time1=3 eg6_fil1_gain=-1 + eg6_time1=3 eg6_fil1gain=-1 )"); const std::string graph = synth.getResources().modMatrix.toDotGraph(); From 4a189ae53ee2d39a61b733a6b2f084bcbd28919f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 26 Sep 2020 20:06:47 +0200 Subject: [PATCH 309/445] ci: try uninstall java before updating homebrew --- .travis/install_osx.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis/install_osx.sh b/.travis/install_osx.sh index d69b8249..466556d7 100755 --- a/.travis/install_osx.sh +++ b/.travis/install_osx.sh @@ -3,6 +3,7 @@ set -ex sudo ln -s /usr/local /opt/local +brew cask uninstall --force java brew update brew upgrade cmake brew install python || brew link --overwrite python From 83f777b8d685ffab7406bf491eda2cbb48bd1107 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 26 Sep 2020 20:44:48 +0200 Subject: [PATCH 310/445] Attempt to accelerate build with Homebrew --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1fb67fdf..0c2e722b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,9 @@ dist: bionic cache: directories: - vst/download/ + # macOS Homebrew + - $HOME/Library/Caches/Homebrew + - /usr/local/Homebrew jobs: include: @@ -48,6 +51,11 @@ jobs: install: .travis/install_osx.sh script: .travis/script_osx.sh after_success: .travis/prepare_osx.sh + before_cache: + - brew cleanup + # Credit https://discourse.brew.sh/t/best-practice-for-homebrew-on-travis-brew-update-is-5min-to-build-time/5215/9 + # Cache only .git files under "/usr/local/Homebrew" so "brew update" does not take 5min every build + - find /usr/local/Homebrew \! -regex ".+\.git.+" -delete - name: "MOD devices arm" env: From ec1032107c993b08e69ede358fe64eabe340d3ab Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 26 Sep 2020 13:40:20 +0200 Subject: [PATCH 311/445] Change the VST3 architecture for ARM to match RPM specification The suffix `hl` indicates a little-endian processor with FPU --- cmake/VSTConfig.cmake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/VSTConfig.cmake b/cmake/VSTConfig.cmake index dd2ae1cb..2d44ad95 100644 --- a/cmake/VSTConfig.cmake +++ b/cmake/VSTConfig.cmake @@ -30,10 +30,10 @@ if(NOT VST3_PACKAGE_ARCHITECTURE) else() set(VST3_PACKAGE_ARCHITECTURE "i386") endif() - elseif(VST3_SYSTEM_PROCESSOR MATCHES "^(armv7l)$") - set(VST3_PACKAGE_ARCHITECTURE "armv7l") + elseif(VST3_SYSTEM_PROCESSOR MATCHES "^(armv[0-9]+)") + string(REGEX REPLACE "^(armv[0-9]+).*$" "\\1hl" VST3_PACKAGE_ARCHITECTURE "${VST3_SYSTEM_PROCESSOR}") elseif(VST3_SYSTEM_PROCESSOR MATCHES "^(aarch64)$") - set(VST3_PACKAGE_ARCHITECTURE "aarch64") + set(VST3_PACKAGE_ARCHITECTURE "aarch64") else() message(FATAL_ERROR "We don't know this architecture for VST3: ${VST3_SYSTEM_PROCESSOR}.") endif() From 5b28feac0333554b9102656c357e75d71c8f8cf4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 02:11:16 +0200 Subject: [PATCH 312/445] Makefile update, and support of VCV Rack SDK --- common.mk | 267 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ dpf.mk | 222 ++------------------------------------------- rack.mk | 98 ++++++++++++++++++++ 3 files changed, 373 insertions(+), 214 deletions(-) create mode 100644 common.mk create mode 100644 rack.mk diff --git a/common.mk b/common.mk new file mode 100644 index 00000000..a403c542 --- /dev/null +++ b/common.mk @@ -0,0 +1,267 @@ +# Common definitions for builds based on GNU Make + +ifndef SFIZZ_DIR +$(error sfizz: The source directory must be set before including) +endif + +### + +SFIZZ_MACHINE := $(shell $(CC) -dumpmachine) +SFIZZ_PROCESSOR := $(firstword $(subst -, ,$(SFIZZ_MACHINE))) + +ifneq (,$(filter i%86,$(SFIZZ_PROCESSOR))) +SFIZZ_CPU_I386 := 1 +SFIZZ_CPU_I386_OR_X86_64 := 1 +endif +ifneq (,$(filter x86_64,$(SFIZZ_PROCESSOR))) +SFIZZ_CPU_X86_64 := 1 +SFIZZ_CPU_I386_OR_X86_64 := 1 +endif +ifneq (,$(filter arm%,$(SFIZZ_PROCESSOR))) +SFIZZ_CPU_ARM := 1 +SFIZZ_CPU_ARM_OR_AARCH64 := 1 +endif +ifneq (,$(filter aarch64%,$(SFIZZ_PROCESSOR))) +SFIZZ_CPU_AARCH64 := 1 +SFIZZ_CPU_ARM_OR_AARCH64 := 1 +endif + +### + +SFIZZ_C_FLAGS = -I$(SFIZZ_DIR)/src +SFIZZ_CXX_FLAGS = $(SFIZZ_C_FLAGS) + +SFIZZ_SOURCES = \ + src/sfizz/ADSREnvelope.cpp \ + src/sfizz/AudioReader.cpp \ + src/sfizz/Curve.cpp \ + src/sfizz/effects/Apan.cpp \ + src/sfizz/Effects.cpp \ + src/sfizz/modulations/ModId.cpp \ + src/sfizz/modulations/ModKey.cpp \ + src/sfizz/modulations/ModKeyHash.cpp \ + src/sfizz/modulations/ModMatrix.cpp \ + src/sfizz/modulations/sources/ADSREnvelope.cpp \ + src/sfizz/modulations/sources/Controller.cpp \ + src/sfizz/modulations/sources/FlexEnvelope.cpp \ + src/sfizz/modulations/sources/LFO.cpp \ + src/sfizz/effects/Compressor.cpp \ + src/sfizz/effects/Disto.cpp \ + src/sfizz/effects/Eq.cpp \ + src/sfizz/effects/Filter.cpp \ + src/sfizz/effects/Fverb.cpp \ + src/sfizz/effects/Gain.cpp \ + src/sfizz/effects/Gate.cpp \ + src/sfizz/effects/impl/ResonantArrayAVX.cpp \ + src/sfizz/effects/impl/ResonantArray.cpp \ + src/sfizz/effects/impl/ResonantArraySSE.cpp \ + src/sfizz/effects/impl/ResonantStringAVX.cpp \ + src/sfizz/effects/impl/ResonantString.cpp \ + src/sfizz/effects/impl/ResonantStringSSE.cpp \ + src/sfizz/effects/Limiter.cpp \ + src/sfizz/effects/Lofi.cpp \ + src/sfizz/effects/Nothing.cpp \ + src/sfizz/effects/Rectify.cpp \ + src/sfizz/effects/Strings.cpp \ + src/sfizz/effects/Width.cpp \ + src/sfizz/EQPool.cpp \ + src/sfizz/FileId.cpp \ + src/sfizz/FileMetadata.cpp \ + src/sfizz/FilePool.cpp \ + src/sfizz/FilterPool.cpp \ + src/sfizz/FlexEGDescription.cpp \ + src/sfizz/FlexEnvelope.cpp \ + src/sfizz/FloatEnvelopes.cpp \ + src/sfizz/Logger.cpp \ + src/sfizz/LFO.cpp \ + src/sfizz/LFODescription.cpp \ + src/sfizz/MidiState.cpp \ + src/sfizz/OpcodeCleanup.cpp \ + src/sfizz/Opcode.cpp \ + src/sfizz/Oversampler.cpp \ + src/sfizz/Panning.cpp \ + src/sfizz/Parser.cpp \ + src/sfizz/parser/Parser.cpp \ + src/sfizz/parser/ParserPrivate.cpp \ + src/sfizz/PolyphonyGroup.cpp \ + src/sfizz/PowerFollower.cpp \ + src/sfizz/Region.cpp \ + src/sfizz/RegionSet.cpp \ + src/sfizz/RTSemaphore.cpp \ + src/sfizz/ScopedFTZ.cpp \ + src/sfizz/sfizz.cpp \ + src/sfizz/sfizz_wrapper.cpp \ + src/sfizz/SfzFilter.cpp \ + src/sfizz/SfzHelpers.cpp \ + src/sfizz/SIMDHelpers.cpp \ + src/sfizz/simd/HelpersSSE.cpp \ + src/sfizz/simd/HelpersAVX.cpp \ + src/sfizz/Smoothers.cpp \ + src/sfizz/Synth.cpp \ + src/sfizz/Tuning.cpp \ + src/sfizz/utility/SpinMutex.cpp \ + src/sfizz/Voice.cpp \ + src/sfizz/VoiceStealing.cpp \ + src/sfizz/Wavetables.cpp + +### Other internal + +SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/sfizz +SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/external + +# Pkg-config dependency + +SFIZZ_PKG_CONFIG ?= pkg-config + +# Sndfile dependency + +SFIZZ_SNDFILE_C_FLAGS ?= $(shell $(SFIZZ_PKG_CONFIG) --cflags sndfile) +SFIZZ_SNDFILE_CXX_FLAGS ?= $(SFIZZ_SNDFILE_C_FLAGS) +SFIZZ_SNDFILE_LINK_FLAGS ?= $(shell $(SFIZZ_PKG_CONFIG) --libs sndfile) + +SFIZZ_C_FLAGS += $(SFIZZ_SNDFILE_C_FLAGS) +SFIZZ_CXX_FLAGS += $(SFIZZ_SNDFILE_CXX_FLAGS) +SFIZZ_LINK_FLAGS += $(SFIZZ_SNDFILE_LINK_FLAGS) + +### Abseil dependency + +SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/external/abseil-cpp +# absl::base +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/base/internal/cycleclock.cc \ + external/abseil-cpp/absl/base/internal/spinlock.cc \ + external/abseil-cpp/absl/base/internal/sysinfo.cc \ + external/abseil-cpp/absl/base/internal/thread_identity.cc \ + external/abseil-cpp/absl/base/internal/unscaledcycleclock.cc +# absl::exponential_biased +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/base/internal/exponential_biased.cc +# absl::dynamic_annotations +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/base/dynamic_annotations.cc +# absl::malloc_internal +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/base/internal/low_level_alloc.cc +# absl::raw_logging_internal +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/base/internal/raw_logging.cc +# absl::spinlock_wait +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/base/internal/spinlock_wait.cc +# absl::throw_delegate +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/base/internal/throw_delegate.cc +# absl::strings +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/strings/ascii.cc \ + external/abseil-cpp/absl/strings/charconv.cc \ + external/abseil-cpp/absl/strings/escaping.cc \ + external/abseil-cpp/absl/strings/internal/charconv_bigint.cc \ + external/abseil-cpp/absl/strings/internal/charconv_parse.cc \ + external/abseil-cpp/absl/strings/internal/memutil.cc \ + external/abseil-cpp/absl/strings/match.cc \ + external/abseil-cpp/absl/strings/numbers.cc \ + external/abseil-cpp/absl/strings/str_cat.cc \ + external/abseil-cpp/absl/strings/str_replace.cc \ + external/abseil-cpp/absl/strings/str_split.cc \ + external/abseil-cpp/absl/strings/string_view.cc \ + external/abseil-cpp/absl/strings/substitute.cc +# absl::hashtablez_sampler +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/container/internal/hashtablez_sampler.cc \ + external/abseil-cpp/absl/container/internal/hashtablez_sampler_force_weak_definition.cc +# absl::raw_hash_set +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/container/internal/raw_hash_set.cc +# absl::synchronization +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/synchronization/barrier.cc \ + external/abseil-cpp/absl/synchronization/blocking_counter.cc \ + external/abseil-cpp/absl/synchronization/internal/create_thread_identity.cc \ + external/abseil-cpp/absl/synchronization/internal/per_thread_sem.cc \ + external/abseil-cpp/absl/synchronization/internal/waiter.cc \ + external/abseil-cpp/absl/synchronization/notification.cc \ + external/abseil-cpp/absl/synchronization/mutex.cc +# absl::graphcycles_internal +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/synchronization/internal/graphcycles.cc +# absl::time +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/time/civil_time.cc \ + external/abseil-cpp/absl/time/clock.cc \ + external/abseil-cpp/absl/time/duration.cc \ + external/abseil-cpp/absl/time/format.cc \ + external/abseil-cpp/absl/time/time.cc +# absl::time_zone +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_fixed.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_format.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_if.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_impl.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_info.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_libc.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_lookup.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/time_zone_posix.cc \ + external/abseil-cpp/absl/time/internal/cctz/src/zone_info_source.cc +# absl::stacktrace +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/debugging/stacktrace.cc +# absl::symbolize +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/debugging/symbolize.cc +# absl::demangle_internal +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/debugging/internal/demangle.cc +# absl::debugging_internal +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/debugging/internal/address_is_readable.cc \ + external/abseil-cpp/absl/debugging/internal/elf_mem_image.cc \ + external/abseil-cpp/absl/debugging/internal/vdso_support.cc +# absl::hash +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/hash/internal/hash.cc +# absl::city +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/hash/internal/city.cc +# absl::int128 +SFIZZ_SOURCES += \ + external/abseil-cpp/absl/numeric/int128.cc + +### Spline dependency + +SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/external/spline +SFIZZ_SOURCES += src/external/spline/spline/spline.cpp + +### Cpuid dependency + +SFIZZ_C_FLAGS += \ + -I$(SFIZZ_DIR)/src/external/cpuid/src \ + -I$(SFIZZ_DIR)/src/external/cpuid/platform/src +SFIZZ_SOURCES += \ + src/external/cpuid/src/cpuid/cpuinfo.cpp \ + src/external/cpuid/src/cpuid/version.cpp + +### Pugixml dependency + +SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/external/pugixml/src +SFIZZ_SOURCES += src/external/pugixml/src/pugixml.cpp + +### Kissfft dependency + +SFIZZ_C_FLAGS += \ + -I$(SFIZZ_DIR)/src/external/kiss_fft \ + -I$(SFIZZ_DIR)/src/external/kiss_fft/tools +SFIZZ_SOURCES += \ + src/external/kiss_fft/kiss_fft.c \ + src/external/kiss_fft/tools/kiss_fftr.c + +### Surge tuning library dependency + +SFIZZ_CXX_FLAGS += \ + -I$(SFIZZ_DIR)/src/external/tunings/include +SFIZZ_SOURCES += \ + src/external/tunings/src/Tunings.cpp + +### jsl dependency +SFIZZ_CXX_FLAGS += \ + -I$(SFIZZ_DIR)/external/jsl/include diff --git a/dpf.mk b/dpf.mk index 71d4555f..d5361692 100644 --- a/dpf.mk +++ b/dpf.mk @@ -37,15 +37,20 @@ SFIZZ_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) SFIZZ_BUILD_DIR := $(SFIZZ_DIR)/dpf-build - -SFIZZ_C_FLAGS = -I$(SFIZZ_DIR)/src -SFIZZ_CXX_FLAGS = $(SFIZZ_C_FLAGS) SFIZZ_LINK_FLAGS = $(SFIZZ_BUILD_DIR)/libsfizz.a +SFIZZ_PKG_CONFIG ?= $(PKG_CONFIG) +include $(SFIZZ_DIR)/common.mk ifeq ($(LINUX),true) +SFIZZ_C_FLAGS += -pthread +SFIZZ_CXX_FLAGS += -pthread SFIZZ_LINK_FLAGS += -pthread endif +ifeq ($(LINUX),true) +SFIZZ_LINK_FLAGS += -lm +endif + sfizz-all: sfizz-lib sfizz-lib: $(SFIZZ_BUILD_DIR)/libsfizz.a @@ -55,217 +60,6 @@ sfizz-clean: .PHONY: sfizz-all sfizz-lib sfizz-clean -SFIZZ_SOURCES = \ - src/sfizz/ADSREnvelope.cpp \ - src/sfizz/AudioReader.cpp \ - src/sfizz/Curve.cpp \ - src/sfizz/effects/Apan.cpp \ - src/sfizz/Effects.cpp \ - src/sfizz/modulations/ModId.cpp \ - src/sfizz/modulations/ModKey.cpp \ - src/sfizz/modulations/ModKeyHash.cpp \ - src/sfizz/modulations/ModMatrix.cpp \ - src/sfizz/modulations/sources/ADSREnvelope.cpp \ - src/sfizz/modulations/sources/Controller.cpp \ - src/sfizz/modulations/sources/FlexEGDescription.cpp \ - src/sfizz/modulations/sources/FlexEnvelope.cpp \ - src/sfizz/modulations/sources/LFO.cpp \ - src/sfizz/effects/Compressor.cpp \ - src/sfizz/effects/Disto.cpp \ - src/sfizz/effects/Eq.cpp \ - src/sfizz/effects/Filter.cpp \ - src/sfizz/effects/Fverb.cpp \ - src/sfizz/effects/Gain.cpp \ - src/sfizz/effects/Gate.cpp \ - src/sfizz/effects/impl/ResonantArrayAVX.cpp \ - src/sfizz/effects/impl/ResonantArray.cpp \ - src/sfizz/effects/impl/ResonantArraySSE.cpp \ - src/sfizz/effects/impl/ResonantStringAVX.cpp \ - src/sfizz/effects/impl/ResonantString.cpp \ - src/sfizz/effects/impl/ResonantStringSSE.cpp \ - src/sfizz/effects/Limiter.cpp \ - src/sfizz/effects/Lofi.cpp \ - src/sfizz/effects/Nothing.cpp \ - src/sfizz/effects/Rectify.cpp \ - src/sfizz/effects/Strings.cpp \ - src/sfizz/effects/Width.cpp \ - src/sfizz/EQPool.cpp \ - src/sfizz/FileId.cpp \ - src/sfizz/FileMetadata.cpp \ - src/sfizz/FilePool.cpp \ - src/sfizz/FilterPool.cpp \ - src/sfizz/FlexEnvelope.cpp \ - src/sfizz/FloatEnvelopes.cpp \ - src/sfizz/Logger.cpp \ - src/sfizz/LFO.cpp \ - src/sfizz/LFODescription.cpp \ - src/sfizz/MidiState.cpp \ - src/sfizz/OpcodeCleanup.cpp \ - src/sfizz/Opcode.cpp \ - src/sfizz/Oversampler.cpp \ - src/sfizz/Panning.cpp \ - src/sfizz/Parser.cpp \ - src/sfizz/parser/Parser.cpp \ - src/sfizz/parser/ParserPrivate.cpp \ - src/sfizz/PowerFollower.cpp \ - src/sfizz/Region.cpp \ - src/sfizz/RTSemaphore.cpp \ - src/sfizz/ScopedFTZ.cpp \ - src/sfizz/sfizz.cpp \ - src/sfizz/sfizz_wrapper.cpp \ - src/sfizz/SfzFilter.cpp \ - src/sfizz/SfzHelpers.cpp \ - src/sfizz/SIMDHelpers.cpp \ - src/sfizz/simd/HelpersSSE.cpp \ - src/sfizz/simd/HelpersAVX.cpp \ - src/sfizz/Synth.cpp \ - src/sfizz/Tuning.cpp \ - src/sfizz/utility/SpinMutex.cpp \ - src/sfizz/Voice.cpp \ - src/sfizz/Wavetables.cpp - -### Other internal - -SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/sfizz -SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/external - -# Sndfile dependency - -SFIZZ_C_FLAGS += $(shell $(PKG_CONFIG) --cflags sndfile) -SFIZZ_LINK_FLAGS += $(shell $(PKG_CONFIG) --libs sndfile) - -### Abseil dependency - -SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/external/abseil-cpp -# absl::base -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/base/internal/cycleclock.cc \ - external/abseil-cpp/absl/base/internal/spinlock.cc \ - external/abseil-cpp/absl/base/internal/sysinfo.cc \ - external/abseil-cpp/absl/base/internal/thread_identity.cc \ - external/abseil-cpp/absl/base/internal/unscaledcycleclock.cc -# absl::exponential_biased -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/base/internal/exponential_biased.cc -# absl::dynamic_annotations -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/base/dynamic_annotations.cc -# absl::malloc_internal -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/base/internal/low_level_alloc.cc -# absl::raw_logging_internal -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/base/internal/raw_logging.cc -# absl::spinlock_wait -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/base/internal/spinlock_wait.cc -# absl::throw_delegate -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/base/internal/throw_delegate.cc -# absl::strings -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/strings/ascii.cc \ - external/abseil-cpp/absl/strings/charconv.cc \ - external/abseil-cpp/absl/strings/escaping.cc \ - external/abseil-cpp/absl/strings/internal/charconv_bigint.cc \ - external/abseil-cpp/absl/strings/internal/charconv_parse.cc \ - external/abseil-cpp/absl/strings/internal/memutil.cc \ - external/abseil-cpp/absl/strings/match.cc \ - external/abseil-cpp/absl/strings/numbers.cc \ - external/abseil-cpp/absl/strings/str_cat.cc \ - external/abseil-cpp/absl/strings/str_replace.cc \ - external/abseil-cpp/absl/strings/str_split.cc \ - external/abseil-cpp/absl/strings/string_view.cc \ - external/abseil-cpp/absl/strings/substitute.cc -# absl::hashtablez_sampler -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/container/internal/hashtablez_sampler.cc \ - external/abseil-cpp/absl/container/internal/hashtablez_sampler_force_weak_definition.cc -# absl::raw_hash_set -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/container/internal/raw_hash_set.cc -# absl::synchronization -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/synchronization/barrier.cc \ - external/abseil-cpp/absl/synchronization/blocking_counter.cc \ - external/abseil-cpp/absl/synchronization/internal/create_thread_identity.cc \ - external/abseil-cpp/absl/synchronization/internal/per_thread_sem.cc \ - external/abseil-cpp/absl/synchronization/internal/waiter.cc \ - external/abseil-cpp/absl/synchronization/notification.cc \ - external/abseil-cpp/absl/synchronization/mutex.cc -# absl::graphcycles_internal -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/synchronization/internal/graphcycles.cc -# absl::time -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/time/civil_time.cc \ - external/abseil-cpp/absl/time/clock.cc \ - external/abseil-cpp/absl/time/duration.cc \ - external/abseil-cpp/absl/time/format.cc \ - external/abseil-cpp/absl/time/time.cc -# absl::time_zone -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_fixed.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_format.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_if.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_impl.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_info.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_libc.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_lookup.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/time_zone_posix.cc \ - external/abseil-cpp/absl/time/internal/cctz/src/zone_info_source.cc -# absl::stacktrace -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/debugging/stacktrace.cc -# absl::symbolize -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/debugging/symbolize.cc -# absl::demangle_internal -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/debugging/internal/demangle.cc -# absl::debugging_internal -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/debugging/internal/address_is_readable.cc \ - external/abseil-cpp/absl/debugging/internal/elf_mem_image.cc \ - external/abseil-cpp/absl/debugging/internal/vdso_support.cc -# absl::hash -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/hash/internal/hash.cc -# absl::city -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/hash/internal/city.cc -# absl::int128 -SFIZZ_SOURCES += \ - external/abseil-cpp/absl/numeric/int128.cc - -### Spline dependency - -SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/external/spline -SFIZZ_SOURCES += src/external/spline/spline/spline.cpp - -### Cpuid dependency - -SFIZZ_C_FLAGS += \ - -I$(SFIZZ_DIR)/src/external/cpuid/src \ - -I$(SFIZZ_DIR)/src/external/cpuid/platform/src -SFIZZ_SOURCES += \ - src/external/cpuid/src/cpuid/cpuinfo.cpp \ - src/external/cpuid/src/cpuid/version.cpp - -### Pugixml dependency - -SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/external/pugixml/src -SFIZZ_SOURCES += src/external/pugixml/src/pugixml.cpp - -### Kissfft dependency - -SFIZZ_C_FLAGS += \ - -I$(SFIZZ_DIR)/src/external/kiss_fft \ - -I$(SFIZZ_DIR)/src/external/kiss_fft/tools -SFIZZ_SOURCES += \ - src/external/kiss_fft/kiss_fft.c \ - src/external/kiss_fft/tools/kiss_fftr.c - ### SFIZZ_OBJECTS = $(SFIZZ_SOURCES:%=$(SFIZZ_BUILD_DIR)/%.o) diff --git a/rack.mk b/rack.mk new file mode 100644 index 00000000..e485e237 --- /dev/null +++ b/rack.mk @@ -0,0 +1,98 @@ + +# +# A build file to help using sfizz with the VCV Rack SDK +# ------------------------------------------------------ +# +# Usage notes: +# +# 1. In the `dep` subfolder of your plugin folder, +# +# Check out the sfizz source code as a submodule +# +# git submodule add https://github.com/sfztools/sfizz.git +# +# 2. At the root of your plugin folder, +# +# Add the following lines, at the bottom of `Makefile`: +# +# # Include the sfizz library +# include dep/sfizz/rack.mk +# CFLAGS += $(SFIZZ_C_FLAGS) +# CXXFLAGS += $(SFIZZ_CXX_FLAGS) +# LDFLAGS += $(SFIZZ_LINK_FLAGS) +# $(TARGET): $(SFIZZ_TARGET) +# +# 3. In the file `Makefile`, +# +# Above the line `include dep/sfizz/rack.mk`, some configuration variables +# may be customized. +# +# SFIZZ_RACK_PLUGIN_DIR = +# SFIZZ_PKG_CONFIG = +# SFIZZ_SNDFILE_C_FLAGS = +# SFIZZ_SNDFILE_CXX_FLAGS = +# SFIZZ_SNDFILE_LINK_FLAGS = + +ifndef RACK_DIR +$(error sfizz: We are not invoked from the Rack SDK) +endif + +SFIZZ_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +SFIZZ_RACK_PLUGIN_DIR ?= . +ifneq ($(shell test -f $(SFIZZ_RACK_PLUGIN_DIR)/plugin.json && echo 1),1) +$(error sfizz: This is not a Rack plugin directory) +endif +SFIZZ_BUILD_DIR := $(SFIZZ_RACK_PLUGIN_DIR)/build/sfizz +include $(SFIZZ_DIR)/common.mk + +### + +ifdef ARCH_LIN +SFIZZ_C_FLAGS += -pthread +SFIZZ_CXX_FLAGS += -pthread +SFIZZ_LINK_FLAGS += -pthread +endif + +ifdef ARCH_LIN +SFIZZ_LINK_FLAGS += -lm +endif + +SFIZZ_TARGET := $(SFIZZ_BUILD_DIR)/libsfizz.a + +### + +SFIZZ_OBJECTS = $(SFIZZ_SOURCES:%=$(SFIZZ_BUILD_DIR)/%.o) + +$(SFIZZ_BUILD_DIR)/libsfizz.a: $(SFIZZ_OBJECTS) + -@mkdir -p $(dir $@) + $(AR) crs $@ $^ + +### + +ifeq ($(SFIZZ_CPU_I386_OR_X86_64),1) + +$(SFIZZ_BUILD_DIR)/%SSE.cpp.o: $(SFIZZ_DIR)/%SSE.cpp + -@mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) $(CXXFLAGS) -msse2 -c -o $@ $< + +$(SFIZZ_BUILD_DIR)/%AVX.cpp.o: $(SFIZZ_DIR)/%AVX.cpp + -@mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) $(CXXFLAGS) -mavx -c -o $@ $< + +endif + +### + +$(SFIZZ_BUILD_DIR)/%.cpp.o: $(SFIZZ_DIR)/%.cpp + -@mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) $(CXXFLAGS) -c -o $@ $< + +$(SFIZZ_BUILD_DIR)/%.cc.o: $(SFIZZ_DIR)/%.cc + -@mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) $(CXXFLAGS) -c -o $@ $< + +$(SFIZZ_BUILD_DIR)/%.c.o: $(SFIZZ_DIR)/%.c + -@mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(CFLAGS) -c -o $@ $< + +-include $(SFIZZ_OBJECTS:%.o=%.d) From d99613052704353a1bc95dd858e5a17952ddfdfc Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 12:51:49 +0200 Subject: [PATCH 313/445] Have the sequence start at the first item --- src/sfizz/Region.cpp | 25 +++++++------------------ tests/RegionActivationT.cpp | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 7d69d202..1b9b0fa7 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -363,7 +363,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) break; case hash("seq_position"): setValueFromOpcode(opcode, sequencePosition, Default::sequenceRange); - sequenceSwitched = (opcode.value == "1"); + sequenceSwitched = false; break; // Region logic: triggers case hash("trigger"): @@ -1571,12 +1571,8 @@ bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue ASSERT(velocity >= 0.0f && velocity <= 1.0f); if (keyswitchRange.containsWithEnd(noteNumber)) { - if (keyswitch) { - if (*keyswitch == noteNumber) - keySwitched = true; - else - keySwitched = false; - } + if (keyswitch) + keySwitched = (*keyswitch == noteNumber); if (keyswitchDown && *keyswitchDown == noteNumber) keySwitched = true; @@ -1588,18 +1584,11 @@ bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue const bool keyOk = keyRange.containsWithEnd(noteNumber); if (keyOk) { // Sequence activation - sequenceCounter += 1; - if ((sequenceCounter % sequenceLength) == sequencePosition - 1) - sequenceSwitched = true; - else - sequenceSwitched = false; + sequenceSwitched = + ((sequenceCounter++ % sequenceLength) == sequencePosition - 1); - if (previousNote) { - if (*previousNote == noteNumber) - previousKeySwitched = true; - else - previousKeySwitched = false; - } + if (previousNote) + previousKeySwitched = (*previousNote == noteNumber); } if (!isSwitchedOn()) diff --git a/tests/RegionActivationT.cpp b/tests/RegionActivationT.cpp index 41990934..e7957ffb 100644 --- a/tests/RegionActivationT.cpp +++ b/tests/RegionActivationT.cpp @@ -216,10 +216,6 @@ TEST_CASE("Region activation", "Region tests") region.parseOpcode({ "seq_length", "2" }); region.parseOpcode({ "seq_position", "1" }); region.parseOpcode({ "key", "40" }); - REQUIRE(region.isSwitchedOn()); - region.registerNoteOn(40, 64_norm, 0.5f); - REQUIRE(!region.isSwitchedOn()); - region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!region.isSwitchedOn()); region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(region.isSwitchedOn()); @@ -229,6 +225,10 @@ TEST_CASE("Region activation", "Region tests") REQUIRE(!region.isSwitchedOn()); region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!region.isSwitchedOn()); + region.registerNoteOn(40, 64_norm, 0.5f); + REQUIRE(region.isSwitchedOn()); + region.registerNoteOff(40, 0_norm, 0.5f); + REQUIRE(region.isSwitchedOn()); } SECTION("Sequences: length 2, position 2") { @@ -237,10 +237,6 @@ TEST_CASE("Region activation", "Region tests") region.parseOpcode({ "key", "40" }); REQUIRE(!region.isSwitchedOn()); region.registerNoteOn(40, 64_norm, 0.5f); - REQUIRE(region.isSwitchedOn()); - region.registerNoteOff(40, 0_norm, 0.5f); - REQUIRE(region.isSwitchedOn()); - region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!region.isSwitchedOn()); region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!region.isSwitchedOn()); @@ -248,6 +244,10 @@ TEST_CASE("Region activation", "Region tests") REQUIRE(region.isSwitchedOn()); region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(region.isSwitchedOn()); + region.registerNoteOn(40, 64_norm, 0.5f); + REQUIRE(!region.isSwitchedOn()); + region.registerNoteOff(40, 0_norm, 0.5f); + REQUIRE(!region.isSwitchedOn()); } SECTION("Sequences: length 3, position 2") { @@ -256,6 +256,10 @@ TEST_CASE("Region activation", "Region tests") region.parseOpcode({ "key", "40" }); REQUIRE(!region.isSwitchedOn()); region.registerNoteOn(40, 64_norm, 0.5f); + REQUIRE(!region.isSwitchedOn()); + region.registerNoteOff(40, 0_norm, 0.5f); + REQUIRE(!region.isSwitchedOn()); + region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(region.isSwitchedOn()); region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(region.isSwitchedOn()); @@ -267,9 +271,5 @@ TEST_CASE("Region activation", "Region tests") REQUIRE(!region.isSwitchedOn()); region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!region.isSwitchedOn()); - region.registerNoteOn(40, 64_norm, 0.5f); - REQUIRE(region.isSwitchedOn()); - region.registerNoteOff(40, 0_norm, 0.5f); - REQUIRE(region.isSwitchedOn()); } } From 4a90ae166a9dac56ad5249a47045164bfe8ed1e5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 15:02:56 +0200 Subject: [PATCH 314/445] Fix a DBG in case the native path is wide string --- src/sfizz/FilePool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 9750da2c..0fedc45b 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -180,7 +180,7 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept DBG("Error extracting the new relative path for " << filename << " (Error code: " << ec.message() << ")"); return false; } - DBG("Updating " << filename << " to " << newPath.native()); + DBG("Updating " << filename << " to " << newPath); filename = newPath.string(); return true; #endif From 6223fe53363f607d22bc1370929782484e7d38d5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 15:11:47 +0200 Subject: [PATCH 315/445] Fix case-insensitive path comparisons when using wide string --- src/sfizz/FilePool.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 0fedc45b..3ec13faa 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -160,8 +160,13 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept } auto searchPredicate = [&part](const fs::directory_entry &ent) -> bool { +#if !defined(GHC_USE_WCHAR_T) return absl::EqualsIgnoreCase( ent.path().filename().native(), part.native()); +#else + return absl::EqualsIgnoreCase( + ent.path().filename().u8string(), part.u8string()); +#endif }; while (it != fs::directory_iterator{} && !searchPredicate(*it)) From 283a7a39c74dfb02aad3f2d5624e6a610f9d019e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 15:34:12 +0200 Subject: [PATCH 316/445] Make filN_random real-valued, and bipolar --- src/sfizz/Defaults.h | 4 ++-- src/sfizz/FilterDescription.h | 2 +- src/sfizz/FilterPool.cpp | 4 ++-- src/sfizz/FilterPool.h | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 1550e749..e0ba851b 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -160,7 +160,7 @@ namespace Default constexpr float filterGain { 0 }; constexpr int filterKeytrack { 0 }; constexpr uint8_t filterKeycenter { 60 }; - constexpr int filterRandom { 0 }; + constexpr float filterRandom { 0 }; constexpr int filterVeltrack { 0 }; constexpr float filterCutoffCC { 0 }; constexpr float filterResonanceCC { 0 }; @@ -170,7 +170,7 @@ namespace Default constexpr Range filterGainRange { -96.0f, 96.0f }; constexpr Range filterGainModRange { -96.0f, 96.0f }; constexpr Range filterKeytrackRange { 0, 1200 }; - constexpr Range filterRandomRange { 0, 9600 }; + constexpr Range filterRandomRange { 0, 9600 }; constexpr Range filterVeltrackRange { -9600, 9600 }; constexpr Range filterResonanceRange { 0.0f, 96.0f }; constexpr Range filterResonanceModRange { 0.0f, 96.0f }; diff --git a/src/sfizz/FilterDescription.h b/src/sfizz/FilterDescription.h index f78b80d3..2f3193a0 100644 --- a/src/sfizz/FilterDescription.h +++ b/src/sfizz/FilterDescription.h @@ -20,7 +20,7 @@ struct FilterDescription int keytrack { Default::filterKeytrack }; uint8_t keycenter { Default::filterKeycenter }; int veltrack { Default::filterVeltrack }; - int random { Default::filterRandom }; + float random { Default::filterRandom }; FilterType type { FilterType::kFilterLpf2p }; }; } diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index c5eadcef..2463e721 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -30,8 +30,8 @@ void sfz::FilterHolder::setup(const Region& region, unsigned filterId, int noteN // Setup the base values baseCutoff = description->cutoff; if (description->random != 0) { - dist.param(filterRandomDist::param_type(0, description->random)); - baseCutoff *= centsFactor(dist(Random::randomGenerator)); + fast_real_distribution dist { -description->random, description->random }; + baseCutoff *= centsFactor(dist(Random::randomGenerator)); } const auto keytrack = description->keytrack * (noteNumber - description->keycenter); baseCutoff *= centsFactor(keytrack); diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index f11ede24..76074a0c 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -54,8 +54,6 @@ private: ModMatrix::TargetId cutoffTarget; ModMatrix::TargetId resonanceTarget; bool prepared { false }; - using filterRandomDist = std::uniform_int_distribution; - filterRandomDist dist { 0, sfz::Default::filterRandom }; }; } From afc0b3fc56991b7273422cd6158688692720b751 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 15:35:46 +0200 Subject: [PATCH 317/445] Also make pitch_random real-valued --- src/sfizz/Defaults.h | 4 ++-- src/sfizz/Region.cpp | 2 +- src/sfizz/Region.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index e0ba851b..540b6944 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -199,8 +199,8 @@ namespace Default constexpr uint8_t pitchKeycenter { 60 }; constexpr int pitchKeytrack { 100 }; constexpr Range pitchKeytrackRange { -1200, 1200 }; - constexpr int pitchRandom { 0 }; - constexpr Range pitchRandomRange { 0, 9600 }; + constexpr float pitchRandom { 0 }; + constexpr Range pitchRandomRange { 0, 9600 }; constexpr int pitchVeltrack { 0 }; constexpr Range pitchVeltrackRange { -9600, 9600 }; constexpr int transpose { 0 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 7d69d202..ee01cccc 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1714,7 +1714,7 @@ float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - std::uniform_int_distribution pitchDistribution { -pitchRandom, pitchRandom }; + fast_real_distribution pitchDistribution { -pitchRandom, pitchRandom }; auto pitchVariationInCents = pitchKeytrack * (noteNumber - pitchKeycenter); // note difference with pitch center pitchVariationInCents += tune; // sample tuning pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index fabe91c4..76840c53 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -409,7 +409,7 @@ struct Region { uint8_t pitchKeycenter { Default::pitchKeycenter }; // pitch_keycenter bool pitchKeycenterFromSample { false }; int pitchKeytrack { Default::pitchKeytrack }; // pitch_keytrack - int pitchRandom { Default::pitchRandom }; // pitch_random + float pitchRandom { Default::pitchRandom }; // pitch_random int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack int transpose { Default::transpose }; // transpose int tune { Default::tune }; // tune From b73955fe1ca6440f6e1c87b25f32b981efcddcce Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 17:06:12 +0200 Subject: [PATCH 318/445] Fix a preprocessor conditional for WIN32 --- src/sfizz/FilePool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 3ec13faa..0fed17a0 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -133,7 +133,7 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept if (fs::exists(path, ec)) return true; -#if WIN32 +#if defined(_WIN32) return false; #else fs::path oldPath = std::move(path); From fcca6317ecae5c8c6368e65eca6c96182a4dfb8e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 17:14:54 +0200 Subject: [PATCH 319/445] mk: reorganize the link flags, add dbghelp under Windows --- common.mk | 26 ++++++++++++++++++++++++++ dpf.mk | 10 ---------- rack.mk | 10 ---------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/common.mk b/common.mk index a403c542..c5fba852 100644 --- a/common.mk +++ b/common.mk @@ -26,6 +26,13 @@ SFIZZ_CPU_AARCH64 := 1 SFIZZ_CPU_ARM_OR_AARCH64 := 1 endif +ifneq (,$(findstring linux,$(SFIZZ_MACHINE))) +SFIZZ_OS_LINUX := 1 +endif +ifneq (,$(findstring mingw,$(SFIZZ_MACHINE))) +SFIZZ_OS_WINDOWS := 1 +endif + ### SFIZZ_C_FLAGS = -I$(SFIZZ_DIR)/src @@ -227,6 +234,10 @@ SFIZZ_SOURCES += \ SFIZZ_SOURCES += \ external/abseil-cpp/absl/numeric/int128.cc +ifdef SFIZZ_OS_WINDOWS +SFIZZ_LINK_FLAGS += -ldbghelp +endif + ### Spline dependency SFIZZ_C_FLAGS += -I$(SFIZZ_DIR)/src/external/spline @@ -263,5 +274,20 @@ SFIZZ_SOURCES += \ src/external/tunings/src/Tunings.cpp ### jsl dependency + SFIZZ_CXX_FLAGS += \ -I$(SFIZZ_DIR)/external/jsl/include + +### math dependency + +ifdef SFIZZ_OS_LINUX +SFIZZ_LINK_FLAGS += -lm +endif + +### pthread dependency + +ifdef SFIZZ_OS_LINUX +SFIZZ_C_FLAGS += -pthread +SFIZZ_CXX_FLAGS += -pthread +SFIZZ_LINK_FLAGS += -pthread +endif diff --git a/dpf.mk b/dpf.mk index d5361692..915db166 100644 --- a/dpf.mk +++ b/dpf.mk @@ -41,16 +41,6 @@ SFIZZ_LINK_FLAGS = $(SFIZZ_BUILD_DIR)/libsfizz.a SFIZZ_PKG_CONFIG ?= $(PKG_CONFIG) include $(SFIZZ_DIR)/common.mk -ifeq ($(LINUX),true) -SFIZZ_C_FLAGS += -pthread -SFIZZ_CXX_FLAGS += -pthread -SFIZZ_LINK_FLAGS += -pthread -endif - -ifeq ($(LINUX),true) -SFIZZ_LINK_FLAGS += -lm -endif - sfizz-all: sfizz-lib sfizz-lib: $(SFIZZ_BUILD_DIR)/libsfizz.a diff --git a/rack.mk b/rack.mk index e485e237..4f8d2512 100644 --- a/rack.mk +++ b/rack.mk @@ -47,16 +47,6 @@ include $(SFIZZ_DIR)/common.mk ### -ifdef ARCH_LIN -SFIZZ_C_FLAGS += -pthread -SFIZZ_CXX_FLAGS += -pthread -SFIZZ_LINK_FLAGS += -pthread -endif - -ifdef ARCH_LIN -SFIZZ_LINK_FLAGS += -lm -endif - SFIZZ_TARGET := $(SFIZZ_BUILD_DIR)/libsfizz.a ### From b65583774f6b7957327ac6d57e0daf862392b2ec Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 17:19:18 +0200 Subject: [PATCH 320/445] mk: add Apple to OS detections [ci skip] --- common.mk | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common.mk b/common.mk index c5fba852..99a83ee1 100644 --- a/common.mk +++ b/common.mk @@ -29,6 +29,9 @@ endif ifneq (,$(findstring linux,$(SFIZZ_MACHINE))) SFIZZ_OS_LINUX := 1 endif +ifneq (,$(findstring apple,$(SFIZZ_MACHINE))) +SFIZZ_OS_APPLE := 1 +endif ifneq (,$(findstring mingw,$(SFIZZ_MACHINE))) SFIZZ_OS_WINDOWS := 1 endif From 0717c2b15df01ef8b58524b3be7a7ff322d09af8 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 19:03:16 +0200 Subject: [PATCH 321/445] CI: Use Travis Homebrew addons instead install script --- .travis.yml | 7 +++++++ .travis/install_osx.sh | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0c2e722b..128f0d63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,13 @@ jobs: - name: "macOS" stage: "Build" os: osx + addons: + homebrew: + packages: + - cmake + - libsndfile + - jack + - dylibbundler env: - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" install: .travis/install_osx.sh diff --git a/.travis/install_osx.sh b/.travis/install_osx.sh index 466556d7..64097a26 100755 --- a/.travis/install_osx.sh +++ b/.travis/install_osx.sh @@ -3,9 +3,3 @@ set -ex sudo ln -s /usr/local /opt/local -brew cask uninstall --force java -brew update -brew upgrade cmake -brew install python || brew link --overwrite python -brew install jack -brew install dylibbundler From 4ada41d54a03b0f900b4e40daab999e22d242290 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 19:37:58 +0200 Subject: [PATCH 322/445] CI: Update osx image, disable cache --- .travis.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 128f0d63..07019851 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ cache: directories: - vst/download/ # macOS Homebrew - - $HOME/Library/Caches/Homebrew - - /usr/local/Homebrew +# - $HOME/Library/Caches/Homebrew +# - /usr/local/Homebrew jobs: include: @@ -46,6 +46,7 @@ jobs: - name: "macOS" stage: "Build" os: osx + osx_image: xcode11.3 addons: homebrew: packages: @@ -58,11 +59,11 @@ jobs: install: .travis/install_osx.sh script: .travis/script_osx.sh after_success: .travis/prepare_osx.sh - before_cache: - - brew cleanup +# before_cache: +# - brew cleanup # Credit https://discourse.brew.sh/t/best-practice-for-homebrew-on-travis-brew-update-is-5min-to-build-time/5215/9 # Cache only .git files under "/usr/local/Homebrew" so "brew update" does not take 5min every build - - find /usr/local/Homebrew \! -regex ".+\.git.+" -delete +# - find /usr/local/Homebrew \! -regex ".+\.git.+" -delete - name: "MOD devices arm" env: From d6dcead9158bc4b0dfc42ee160fbad1d1ac3b948 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 19:59:01 +0200 Subject: [PATCH 323/445] Add libsndfile to sfizz_jack client link options --- clients/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/CMakeLists.txt b/clients/CMakeLists.txt index 859005c0..b3fb981a 100644 --- a/clients/CMakeLists.txt +++ b/clients/CMakeLists.txt @@ -6,7 +6,7 @@ if (SFIZZ_JACK) add_executable (sfizz_jack MidiHelpers.h jack_client.cpp) target_include_directories (sfizz_jack PRIVATE ${JACK_INCLUDE_DIRS}) - target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz jack absl::flags_parse ${JACK_LIBRARIES}) + target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz jack absl::flags_parse sfizz-sndfile ${JACK_LIBRARIES}) sfizz_enable_lto_if_needed (sfizz_jack) install (TARGETS sfizz_jack DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT "jack" OPTIONAL) From 494ce78a586f8a4d400a293ad8b42ebe27cabc06 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 20:12:12 +0200 Subject: [PATCH 324/445] Add sfizz_jack link dirs --- clients/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/CMakeLists.txt b/clients/CMakeLists.txt index b3fb981a..b7628dc4 100644 --- a/clients/CMakeLists.txt +++ b/clients/CMakeLists.txt @@ -7,6 +7,7 @@ if (SFIZZ_JACK) add_executable (sfizz_jack MidiHelpers.h jack_client.cpp) target_include_directories (sfizz_jack PRIVATE ${JACK_INCLUDE_DIRS}) target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz jack absl::flags_parse sfizz-sndfile ${JACK_LIBRARIES}) + link_directories (${JACK_LIBRARY_DIRS}) sfizz_enable_lto_if_needed (sfizz_jack) install (TARGETS sfizz_jack DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT "jack" OPTIONAL) From 67804314fe0dd6bb87985c967009a118417b17f4 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 20:30:48 +0200 Subject: [PATCH 325/445] CI: Remove osx opt symlink --- .travis.yml | 1 - .travis/install_osx.sh | 5 ----- 2 files changed, 6 deletions(-) delete mode 100755 .travis/install_osx.sh diff --git a/.travis.yml b/.travis.yml index 07019851..28a12a11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,7 +56,6 @@ jobs: - dylibbundler env: - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" - install: .travis/install_osx.sh script: .travis/script_osx.sh after_success: .travis/prepare_osx.sh # before_cache: diff --git a/.travis/install_osx.sh b/.travis/install_osx.sh deleted file mode 100755 index 64097a26..00000000 --- a/.travis/install_osx.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -ex - -sudo ln -s /usr/local /opt/local From 7b294b71bd639f8fd240c7a38887cee6ce1df1ba Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 20:32:05 +0200 Subject: [PATCH 326/445] Typo in sfizz_jack CMakeLists --- clients/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/CMakeLists.txt b/clients/CMakeLists.txt index b7628dc4..3e4c9322 100644 --- a/clients/CMakeLists.txt +++ b/clients/CMakeLists.txt @@ -6,7 +6,7 @@ if (SFIZZ_JACK) add_executable (sfizz_jack MidiHelpers.h jack_client.cpp) target_include_directories (sfizz_jack PRIVATE ${JACK_INCLUDE_DIRS}) - target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz jack absl::flags_parse sfizz-sndfile ${JACK_LIBRARIES}) + target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz absl::flags_parse sfizz-sndfile ${JACK_LIBRARIES}) link_directories (${JACK_LIBRARY_DIRS}) sfizz_enable_lto_if_needed (sfizz_jack) install (TARGETS sfizz_jack DESTINATION ${CMAKE_INSTALL_BINDIR} From 471a9ca615cdd3e4ce39cd83a4b2475bf03c5555 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 21:24:08 +0200 Subject: [PATCH 327/445] sfizz_jack: fix link_directories() in CMakeLists --- clients/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/CMakeLists.txt b/clients/CMakeLists.txt index 3e4c9322..9192234d 100644 --- a/clients/CMakeLists.txt +++ b/clients/CMakeLists.txt @@ -3,11 +3,11 @@ project (sfizz) if (SFIZZ_JACK) find_package(PkgConfig REQUIRED) pkg_check_modules(JACK "jack" REQUIRED) + link_directories (${JACK_LIBRARY_DIRS}) add_executable (sfizz_jack MidiHelpers.h jack_client.cpp) target_include_directories (sfizz_jack PRIVATE ${JACK_INCLUDE_DIRS}) target_link_libraries (sfizz_jack PRIVATE sfizz::sfizz absl::flags_parse sfizz-sndfile ${JACK_LIBRARIES}) - link_directories (${JACK_LIBRARY_DIRS}) sfizz_enable_lto_if_needed (sfizz_jack) install (TARGETS sfizz_jack DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT "jack" OPTIONAL) From 85e3081d82f8d7a716f13a01f0f86caf89bc2075 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 27 Sep 2020 21:45:14 +0200 Subject: [PATCH 328/445] CI: forget about Homebrew cache [ci skip] --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28a12a11..080c1cbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,6 @@ dist: bionic cache: directories: - vst/download/ - # macOS Homebrew -# - $HOME/Library/Caches/Homebrew -# - /usr/local/Homebrew jobs: include: @@ -58,11 +55,6 @@ jobs: - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" script: .travis/script_osx.sh after_success: .travis/prepare_osx.sh -# before_cache: -# - brew cleanup - # Credit https://discourse.brew.sh/t/best-practice-for-homebrew-on-travis-brew-update-is-5min-to-build-time/5215/9 - # Cache only .git files under "/usr/local/Homebrew" so "brew update" does not take 5min every build -# - find /usr/local/Homebrew \! -regex ".+\.git.+" -delete - name: "MOD devices arm" env: From 48ac9384af98fb942780aa33b51912e8ded49f74 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 22:57:31 +0200 Subject: [PATCH 329/445] Update ScopedFTZ asm for arm64 --- src/sfizz/ScopedFTZ.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sfizz/ScopedFTZ.cpp b/src/sfizz/ScopedFTZ.cpp index 1f5b1c58..22c2c1e4 100644 --- a/src/sfizz/ScopedFTZ.cpp +++ b/src/sfizz/ScopedFTZ.cpp @@ -19,10 +19,14 @@ ScopedFTZ::ScopedFTZ() unsigned mask = _MM_DENORMALS_ZERO_MASK | _MM_FLUSH_ZERO_MASK; registerState = _mm_getcsr(); _mm_setcsr((registerState & (~mask)) | mask); -#elif SFIZZ_HAVE_NEON +#elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_ARM intptr_t mask = (1 << 24); asm volatile("vmrs %0, fpscr" : "=r"(registerState)); - asm volatile("vmsr fpscr, %0" : : "ri"((registerState & (~mask)) | mask)); + asm volatile("vmsr fpscr, %0" : : "r"((registerState & (~mask)) | mask)); +#elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_AARCH64 + intptr_t mask = (1 << 24); + asm volatile("mrs %0, fpcr" : "=r"(registerState)); + asm volatile("msr fpcr, %0" : : "r"((registerState & (~mask)) | mask)); #endif } @@ -30,7 +34,9 @@ ScopedFTZ::~ScopedFTZ() { #if SFIZZ_HAVE_SSE _mm_setcsr(registerState); -#elif SFIZZ_HAVE_NEON - asm volatile("vmrs %0, fpscr" : : "ri"(registerState)); +#elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_ARM + asm volatile("vmrs %0, fpscr" : : "r"(registerState)); +#elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_AARCH64 + asm volatile("mrs %0, fpcr" : : "r"(registerState)); #endif } From 8ebd256290006dbe57bfe0fe93877c0b3051bd31 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 23:07:55 +0200 Subject: [PATCH 330/445] FPCR register is 64-bit --- src/sfizz/ScopedFTZ.cpp | 8 ++++---- src/sfizz/ScopedFTZ.h | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sfizz/ScopedFTZ.cpp b/src/sfizz/ScopedFTZ.cpp index 22c2c1e4..46ca9424 100644 --- a/src/sfizz/ScopedFTZ.cpp +++ b/src/sfizz/ScopedFTZ.cpp @@ -18,13 +18,13 @@ ScopedFTZ::ScopedFTZ() #if SFIZZ_HAVE_SSE unsigned mask = _MM_DENORMALS_ZERO_MASK | _MM_FLUSH_ZERO_MASK; registerState = _mm_getcsr(); - _mm_setcsr((registerState & (~mask)) | mask); + _mm_setcsr((static_cast(registerState) & (~mask)) | mask); #elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_ARM - intptr_t mask = (1 << 24); + uintptr_t mask = 1u << 24; asm volatile("vmrs %0, fpscr" : "=r"(registerState)); asm volatile("vmsr fpscr, %0" : : "r"((registerState & (~mask)) | mask)); #elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_AARCH64 - intptr_t mask = (1 << 24); + uintptr_t mask = 1u << 24; asm volatile("mrs %0, fpcr" : "=r"(registerState)); asm volatile("msr fpcr, %0" : : "r"((registerState & (~mask)) | mask)); #endif @@ -33,7 +33,7 @@ ScopedFTZ::ScopedFTZ() ScopedFTZ::~ScopedFTZ() { #if SFIZZ_HAVE_SSE - _mm_setcsr(registerState); + _mm_setcsr(static_cast(registerState)); #elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_ARM asm volatile("vmrs %0, fpscr" : : "r"(registerState)); #elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_AARCH64 diff --git a/src/sfizz/ScopedFTZ.h b/src/sfizz/ScopedFTZ.h index 6fea43b0..15f024d9 100644 --- a/src/sfizz/ScopedFTZ.h +++ b/src/sfizz/ScopedFTZ.h @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include /** * @brief Flush floating points to zero and disable denormals as an RAII helper. @@ -16,5 +17,5 @@ public: ScopedFTZ(); ~ScopedFTZ(); private: - unsigned registerState; + uintptr_t registerState; }; From 35fc1f01052b5165e0265b8ca64917b851e92faf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 28 Sep 2020 00:28:54 +0200 Subject: [PATCH 331/445] Detect NEON on aarch64 gcc --- src/sfizz/SIMDConfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/SIMDConfig.h b/src/sfizz/SIMDConfig.h index d06d6615..3f4c2ccc 100644 --- a/src/sfizz/SIMDConfig.h +++ b/src/sfizz/SIMDConfig.h @@ -35,7 +35,7 @@ # define SFIZZ_DETECT_SSE2 0 # define SFIZZ_DETECT_AVX 0 # endif -# if defined(__ARM_NEON__) +# if defined(__ARM_NEON__) || defined(__ARM_NEON) # define SFIZZ_DETECT_NEON 1 # else # define SFIZZ_DETECT_NEON 0 From 0d49fc1176d41b3354b0655576763e1cffd40333 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 28 Sep 2020 01:49:51 +0200 Subject: [PATCH 332/445] Back to a detection strategy for ARM VST which should work in most cases --- cmake/VSTConfig.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/VSTConfig.cmake b/cmake/VSTConfig.cmake index 2d44ad95..6eea1a0a 100644 --- a/cmake/VSTConfig.cmake +++ b/cmake/VSTConfig.cmake @@ -30,8 +30,8 @@ if(NOT VST3_PACKAGE_ARCHITECTURE) else() set(VST3_PACKAGE_ARCHITECTURE "i386") endif() - elseif(VST3_SYSTEM_PROCESSOR MATCHES "^(armv[0-9]+)") - string(REGEX REPLACE "^(armv[0-9]+).*$" "\\1hl" VST3_PACKAGE_ARCHITECTURE "${VST3_SYSTEM_PROCESSOR}") + elseif(VST3_SYSTEM_PROCESSOR MATCHES "^(armv[3-8][a-z]*)$") + set(VST3_PACKAGE_ARCHITECTURE "${VST3_SYSTEM_PROCESSOR}") elseif(VST3_SYSTEM_PROCESSOR MATCHES "^(aarch64)$") set(VST3_PACKAGE_ARCHITECTURE "aarch64") else() From d18da2060c2bff779c15b93519f25bcc41667667 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 28 Sep 2020 14:08:41 +0200 Subject: [PATCH 333/445] Add support of some Cakewalk aliases --- src/sfizz/OpcodeCleanup.cpp | 1375 ++++++++++++++++++++--------------- src/sfizz/OpcodeCleanup.re | 11 + src/sfizz/Region.cpp | 2 +- tests/OpcodeT.cpp | 5 + 4 files changed, 793 insertions(+), 600 deletions(-) diff --git a/src/sfizz/OpcodeCleanup.cpp b/src/sfizz/OpcodeCleanup.cpp index ce293269..459ce514 100644 --- a/src/sfizz/OpcodeCleanup.cpp +++ b/src/sfizz/OpcodeCleanup.cpp @@ -1,4 +1,4 @@ -/* Generated by re2c 1.3 on Fri Sep 18 00:52:43 2020 */ +/* Generated by re2c 2.0.3 on Mon Sep 28 14:07:14 2020 */ #line 1 "src/sfizz/OpcodeCleanup.re" /* -*- mode: c++; -*- */ // SPDX-License-Identifier: BSD-2-Clause @@ -174,11 +174,12 @@ end_region_oncc: //-------------------------------------------------------------------------- if (scope == kOpcodeScopeRegion) { + again_region: YYCURSOR = opcode.c_str(); -#line 182 "src/sfizz/OpcodeCleanup.cpp" +#line 183 "src/sfizz/OpcodeCleanup.cpp" { char yych; yych = *YYCURSOR; @@ -218,11 +219,11 @@ end_region_oncc: yy19: ++YYCURSOR; yy20: -#line 176 "src/sfizz/OpcodeCleanup.re" +#line 187 "src/sfizz/OpcodeCleanup.re" { goto end_region; } -#line 226 "src/sfizz/OpcodeCleanup.cpp" +#line 227 "src/sfizz/OpcodeCleanup.cpp" yy21: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { @@ -658,7 +659,7 @@ yy78: yy79: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy111; + case 'a': goto yy110; default: goto yy34; } yy80: @@ -674,7 +675,7 @@ yy80: case '7': case '8': case '9': goto yy80; - case '_': goto yy112; + case '_': goto yy111; default: goto yy34; } yy82: @@ -682,37 +683,37 @@ yy82: switch (yych) { case 'e': yyt1 = YYCURSOR; - goto yy113; + goto yy112; case 'm': yyt1 = YYCURSOR; - goto yy114; + goto yy113; case 's': yyt1 = YYCURSOR; - goto yy115; + goto yy114; default: goto yy34; } yy83: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy116; + case 'y': goto yy115; default: goto yy34; } yy84: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy117; + case 'o': goto yy116; default: goto yy34; } yy85: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy118; + case 'i': goto yy117; default: goto yy34; } yy86: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy118; + case 'o': goto yy117; default: goto yy34; } yy87: @@ -724,13 +725,13 @@ yy87: yy88: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy119; + case 'p': goto yy118; default: goto yy34; } yy89: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy120; + case 'n': goto yy119; default: goto yy34; } yy90: @@ -738,76 +739,76 @@ yy90: switch (yych) { case 0x00: yyt2 = yyt3 = NULL; - goto yy121; + goto yy120; case '_': yyt3 = YYCURSOR; - goto yy123; + goto yy122; default: goto yy34; } yy91: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy125; + case '_': goto yy124; default: goto yy34; } yy92: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy126; + case 'o': goto yy125; default: goto yy34; } yy93: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy127; + case 'o': goto yy126; default: goto yy34; } yy94: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy128; + case 'p': goto yy127; default: goto yy34; } yy95: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy129; + case 'f': goto yy128; default: goto yy34; } yy96: yych = *++YYCURSOR; switch (yych) { - case 'u': goto yy130; + case 'u': goto yy129; default: goto yy34; } yy97: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy131; + case 'e': goto yy130; default: goto yy34; } yy98: yych = *++YYCURSOR; switch (yych) { - case 'w': goto yy132; + case 'w': goto yy131; default: goto yy34; } yy99: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy133; + case 'r': goto yy132; default: goto yy34; } yy100: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy134; + case 'a': goto yy133; default: goto yy34; } yy101: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy135; + case 'y': goto yy134; default: goto yy34; } yy102: @@ -817,12 +818,12 @@ yy102: yypmatch[0] = yyt1 - 4; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 158 "src/sfizz/OpcodeCleanup.re" +#line 169 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil1_", group(1)); goto end_region; } -#line 826 "src/sfizz/OpcodeCleanup.cpp" +#line 827 "src/sfizz/OpcodeCleanup.cpp" yy104: yych = *++YYCURSOR; yy105: @@ -831,7 +832,7 @@ yy105: yy106: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy136; + case 'p': goto yy135; default: goto yy34; } yy107: @@ -841,479 +842,528 @@ yy107: yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 140 "src/sfizz/OpcodeCleanup.re" +#line 146 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("volume", group(1)); goto end_region; } -#line 850 "src/sfizz/OpcodeCleanup.cpp" +#line 851 "src/sfizz/OpcodeCleanup.cpp" yy109: yych = *++YYCURSOR; - if (yych <= 0x00) { - yyt2 = YYCURSOR; - goto yy107; + switch (yych) { + case 'r': goto yy138; + default: goto yy137; + } +yy110: + yych = *++YYCURSOR; + switch (yych) { + case 'l': goto yy139; + default: goto yy34; } - goto yy109; yy111: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy137; + case 'c': + yyt2 = YYCURSOR; + goto yy140; + case 'o': + yyt2 = YYCURSOR; + goto yy141; + case 'r': + yyt2 = YYCURSOR; + goto yy142; + case 's': + yyt2 = YYCURSOR; + goto yy143; + case 'w': + yyt2 = YYCURSOR; + goto yy144; default: goto yy34; } yy112: yych = *++YYCURSOR; switch (yych) { - case 'c': - yyt2 = YYCURSOR; - goto yy138; - case 'o': - yyt2 = YYCURSOR; - goto yy139; - case 'r': - yyt2 = YYCURSOR; - goto yy140; - case 's': - yyt2 = YYCURSOR; - goto yy141; - case 'w': - yyt2 = YYCURSOR; - goto yy142; + case 'n': goto yy145; default: goto yy34; } yy113: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy143; + case 'o': goto yy146; default: goto yy34; } yy114: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy144; + case 't': goto yy147; default: goto yy34; } yy115: yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy145; - default: goto yy34; - } + if (yych <= 0x00) goto yy148; + goto yy34; yy116: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy146; - goto yy34; + switch (yych) { + case 'd': goto yy150; + default: goto yy34; + } yy117: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy148; + case 'c': goto yy151; + case 'h': goto yy152; default: goto yy34; } yy118: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy149; - case 'h': goto yy150; + case 'h': goto yy153; default: goto yy34; } yy119: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy151; + case 'a': goto yy154; default: goto yy34; } yy120: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy152; - default: goto yy34; - } -yy121: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 144 "src/sfizz/OpcodeCleanup.re" +#line 150 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("pitch", group(1)); goto end_region; } #line 943 "src/sfizz/OpcodeCleanup.cpp" -yy123: +yy122: yych = *++YYCURSOR; if (yych <= 0x00) { yyt2 = YYCURSOR; - goto yy121; + goto yy120; } - goto yy123; -yy125: + goto yy122; +yy124: yych = *++YYCURSOR; switch (yych) { case 'a': yyt2 = YYCURSOR; - goto yy153; + goto yy155; case 'd': yyt2 = YYCURSOR; - goto yy154; + goto yy156; case 'h': yyt2 = YYCURSOR; - goto yy155; + goto yy157; case 'r': yyt2 = YYCURSOR; - goto yy156; + goto yy158; case 's': yyt2 = YYCURSOR; - goto yy157; + goto yy159; + default: goto yy34; + } +yy125: + yych = *++YYCURSOR; + switch (yych) { + case '_': goto yy160; default: goto yy34; } yy126: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy158; + case 'w': goto yy161; default: goto yy34; } yy127: yych = *++YYCURSOR; - switch (yych) { - case 'w': goto yy159; - default: goto yy34; - } -yy128: - yych = *++YYCURSOR; - if (yych <= 0x00) goto yy160; + if (yych <= 0x00) goto yy162; goto yy34; -yy129: +yy128: yych = *++YYCURSOR; switch (yych) { case 0x00: yyt2 = yyt3 = NULL; - goto yy162; - case '_': - yyt3 = YYCURSOR; goto yy164; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + yyt2 = YYCURSOR; + goto yy166; + case '_': + yyt2 = yyt4 = NULL; + yyt3 = YYCURSOR; + goto yy168; + default: goto yy34; + } +yy129: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy169; default: goto yy34; } yy130: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy166; + case 's': goto yy170; default: goto yy34; } yy131: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy167; + case 'c': goto yy171; default: goto yy34; } yy132: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy168; + case 'e': goto yy172; default: goto yy34; } yy133: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy169; + case 'i': goto yy173; default: goto yy34; } yy134: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy170; + case 'p': goto yy174; default: goto yy34; } yy135: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy171; + case 'e': goto yy175; default: goto yy34; } yy136: yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy172; - default: goto yy34; - } yy137: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy173; - default: goto yy34; + if (yych <= 0x00) { + yyt2 = YYCURSOR; + goto yy107; } + goto yy136; yy138: yych = *++YYCURSOR; switch (yych) { - case 'u': goto yy174; - default: goto yy34; + case 'a': goto yy176; + default: goto yy137; } yy139: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy175; + case 'c': goto yy177; default: goto yy34; } yy140: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy176; - case 'e': goto yy177; + case 'u': goto yy178; default: goto yy34; } yy141: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy178; + case 'f': goto yy179; default: goto yy34; } yy142: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy179; + case 'a': goto yy180; + case 'e': goto yy181; default: goto yy34; } yy143: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy180; + case 'c': goto yy182; default: goto yy34; } yy144: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy181; + case 'a': goto yy183; default: goto yy34; } yy145: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy182; + case 'd': goto yy184; default: goto yy34; } yy146: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy185; + default: goto yy34; + } +yy147: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy186; + default: goto yy34; + } +yy148: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 3; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 118 "src/sfizz/OpcodeCleanup.re" +#line 119 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("off_", group(1)); goto end_region; } -#line 1107 "src/sfizz/OpcodeCleanup.cpp" -yy148: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy116; - default: goto yy34; - } -yy149: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy183; - default: goto yy34; - } +#line 1134 "src/sfizz/OpcodeCleanup.cpp" yy150: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy184; + case 'e': goto yy115; default: goto yy34; } yy151: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy185; + case 'c': goto yy187; default: goto yy34; } yy152: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy186; + case 'd': goto yy188; default: goto yy34; } yy153: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy187; + case 'o': goto yy189; default: goto yy34; } yy154: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy188; + case 'n': goto yy190; default: goto yy34; } yy155: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy189; + case 't': goto yy191; default: goto yy34; } yy156: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy190; + case 'e': goto yy192; default: goto yy34; } yy157: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy191; - case 'u': goto yy192; + case 'o': goto yy193; default: goto yy34; } yy158: yych = *++YYCURSOR; switch (yych) { - case 'd': - yyt2 = YYCURSOR; - goto yy193; - case 'f': - yyt2 = YYCURSOR; - goto yy194; + case 'e': goto yy194; default: goto yy34; } yy159: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy128; + case 't': goto yy195; + case 'u': goto yy196; default: goto yy34; } yy160: + yych = *++YYCURSOR; + switch (yych) { + case 'd': + yyt2 = YYCURSOR; + goto yy197; + case 'f': + yyt2 = YYCURSOR; + goto yy198; + default: goto yy34; + } +yy161: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy127; + default: goto yy34; + } +yy162: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 4; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 122 "src/sfizz/OpcodeCleanup.re" +#line 123 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("bend_", group(1)); goto end_region; } -#line 1198 "src/sfizz/OpcodeCleanup.cpp" -yy162: +#line 1225 "src/sfizz/OpcodeCleanup.cpp" +yy164: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 162 "src/sfizz/OpcodeCleanup.re" +#line 173 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("cutoff1", group(1)); goto end_region; } -#line 1211 "src/sfizz/OpcodeCleanup.cpp" -yy164: - yych = *++YYCURSOR; - if (yych <= 0x00) { - yyt2 = YYCURSOR; - goto yy162; - } - goto yy164; +#line 1238 "src/sfizz/OpcodeCleanup.cpp" yy166: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy195; - default: goto yy34; - } -yy167: - yych = *++YYCURSOR; - switch (yych) { - case 'o': goto yy196; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy166; + case '_': + yyt4 = YYCURSOR; + goto yy199; default: goto yy34; } yy168: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy197; - default: goto yy34; + case 'r': goto yy202; + default: goto yy201; } yy169: yych = *++YYCURSOR; switch (yych) { - case 'q': goto yy132; + case 'o': goto yy203; default: goto yy34; } yy170: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy132; + case 'o': goto yy204; default: goto yy34; } yy171: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy198; + case 'c': goto yy205; default: goto yy34; } yy172: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy199; - goto yy34; + switch (yych) { + case 'q': goto yy131; + default: goto yy34; + } yy173: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy201; + case 'n': goto yy131; default: goto yy34; } yy174: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy202; + case 'e': goto yy206; default: goto yy34; } yy175: yych = *++YYCURSOR; - switch (yych) { - case 'f': goto yy203; - default: goto yy34; - } + if (yych <= 0x00) goto yy207; + goto yy34; yy176: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy204; - default: goto yy34; + case 'n': goto yy209; + default: goto yy137; } yy177: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy205; + case 'c': goto yy210; default: goto yy34; } yy178: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy206; + case 't': goto yy211; default: goto yy34; } yy179: yych = *++YYCURSOR; switch (yych) { - case 'v': goto yy207; + case 'f': goto yy212; default: goto yy34; } yy180: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy208; - goto yy34; + switch (yych) { + case 't': goto yy213; + default: goto yy34; + } yy181: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy180; + case 's': goto yy214; default: goto yy34; } yy182: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy210; + case 'a': goto yy215; default: goto yy34; } yy183: + yych = *++YYCURSOR; + switch (yych) { + case 'v': goto yy216; + default: goto yy34; + } +yy184: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy217; + goto yy34; +yy185: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy184; + default: goto yy34; + } +yy186: + yych = *++YYCURSOR; + switch (yych) { + case 'r': goto yy219; + default: goto yy34; + } +yy187: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1327,90 +1377,110 @@ yy183: case '8': case '9': yyt1 = YYCURSOR; - goto yy211; - default: goto yy34; - } -yy184: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy213; - default: goto yy34; - } -yy185: - yych = *++YYCURSOR; - switch (yych) { - case 'n': goto yy214; - default: goto yy34; - } -yy186: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy215; - default: goto yy34; - } -yy187: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy216; + goto yy220; default: goto yy34; } yy188: yych = *++YYCURSOR; switch (yych) { - case 'c': - case 'l': goto yy217; + case 'c': goto yy222; default: goto yy34; } yy189: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy218; + case 'n': goto yy223; default: goto yy34; } yy190: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy219; + case 'c': goto yy224; default: goto yy34; } yy191: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy220; + case 't': goto yy225; default: goto yy34; } yy192: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy221; + case 'c': + case 'l': goto yy226; default: goto yy34; } yy193: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy222; + case 'l': goto yy227; default: goto yy34; } yy194: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy223; - case 'r': goto yy224; + case 'l': goto yy228; default: goto yy34; } yy195: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy225; + case 'a': goto yy229; default: goto yy34; } yy196: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy226; + case 's': goto yy230; default: goto yy34; } yy197: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy231; + default: goto yy34; + } +yy198: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy232; + case 'r': goto yy233; + default: goto yy34; + } +yy199: + yych = *++YYCURSOR; + switch (yych) { + case 'r': goto yy234; + default: goto yy34; + } +yy200: + yych = *++YYCURSOR; +yy201: + if (yych <= 0x00) { + yyt2 = YYCURSOR; + goto yy164; + } + goto yy200; +yy202: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy235; + default: goto yy201; + } +yy203: + yych = *++YYCURSOR; + switch (yych) { + case 'f': goto yy236; + default: goto yy34; + } +yy204: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy237; + default: goto yy34; + } +yy205: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1424,25 +1494,31 @@ yy197: case '8': case '9': yyt3 = YYCURSOR; - goto yy227; + goto yy238; default: goto yy34; } -yy198: +yy206: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy229; + if (yych <= 0x00) goto yy240; goto yy34; -yy199: +yy207: ++YYCURSOR; yynmatch = 1; yypmatch[0] = YYCURSOR - 8; yypmatch[1] = YYCURSOR; -#line 126 "src/sfizz/OpcodeCleanup.re" +#line 127 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil1_type"); goto end_region; } -#line 1445 "src/sfizz/OpcodeCleanup.cpp" -yy201: +#line 1515 "src/sfizz/OpcodeCleanup.cpp" +yy209: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy242; + default: goto yy137; + } +yy210: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1456,168 +1532,68 @@ yy201: case '8': case '9': yyt1 = YYCURSOR; - goto yy231; + goto yy243; default: goto yy34; } -yy202: +yy211: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy233; + case 'o': goto yy245; default: goto yy34; } -yy203: +yy212: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy234; + case 's': goto yy246; default: goto yy34; } -yy204: +yy213: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy235; + case 'i': goto yy247; default: goto yy34; } -yy205: +yy214: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy236; + case 'o': goto yy248; default: goto yy34; } -yy206: +yy215: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy207; + case 'l': goto yy216; default: goto yy34; } -yy207: +yy216: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy237; + case 'e': goto yy249; default: goto yy34; } -yy208: +yy217: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 4; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 114 "src/sfizz/OpcodeCleanup.re" +#line 115 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("loop_", group(1)); goto end_region; } -#line 1511 "src/sfizz/OpcodeCleanup.cpp" -yy210: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy180; - default: goto yy34; - } -yy211: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: goto yy238; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': goto yy211; - default: goto yy34; - } -yy213: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy240; - default: goto yy34; - } -yy214: - yych = *++YYCURSOR; - switch (yych) { - case 'y': goto yy241; - default: goto yy34; - } -yy215: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy242; - default: goto yy34; - } -yy216: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy243; - default: goto yy34; - } -yy217: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy244; - default: goto yy34; - } -yy218: - yych = *++YYCURSOR; - switch (yych) { - case 'd': goto yy245; - default: goto yy34; - } +#line 1587 "src/sfizz/OpcodeCleanup.cpp" yy219: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy246; + case 't': goto yy184; default: goto yy34; } yy220: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy247; - default: goto yy34; - } -yy221: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy248; - default: goto yy34; - } -yy222: - yych = *++YYCURSOR; - switch (yych) { - case 'p': goto yy249; - default: goto yy34; - } -yy223: - yych = *++YYCURSOR; - switch (yych) { - case 'd': goto yy250; - default: goto yy34; - } -yy224: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy251; - default: goto yy34; - } -yy225: - yych = *++YYCURSOR; - switch (yych) { - case 'f': goto yy252; - default: goto yy34; - } -yy226: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy253; - default: goto yy34; - } -yy227: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: goto yy254; + case 0x00: goto yy250; case '0': case '1': case '2': @@ -1627,26 +1603,144 @@ yy227: case '6': case '7': case '8': - case '9': goto yy227; + case '9': goto yy220; + default: goto yy34; + } +yy222: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy252; + default: goto yy34; + } +yy223: + yych = *++YYCURSOR; + switch (yych) { + case 'y': goto yy253; + default: goto yy34; + } +yy224: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy254; + default: goto yy34; + } +yy225: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy255; + default: goto yy34; + } +yy226: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy256; + default: goto yy34; + } +yy227: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy257; + default: goto yy34; + } +yy228: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy258; default: goto yy34; } yy229: + yych = *++YYCURSOR; + switch (yych) { + case 'r': goto yy259; + default: goto yy34; + } +yy230: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy260; + default: goto yy34; + } +yy231: + yych = *++YYCURSOR; + switch (yych) { + case 'p': goto yy261; + default: goto yy34; + } +yy232: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy262; + default: goto yy34; + } +yy233: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy263; + default: goto yy34; + } +yy234: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy264; + default: goto yy34; + } +yy235: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy265; + default: goto yy201; + } +yy236: + yych = *++YYCURSOR; + switch (yych) { + case 'f': goto yy266; + default: goto yy34; + } +yy237: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy267; + default: goto yy34; + } +yy238: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: goto yy268; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': goto yy238; + default: goto yy34; + } +yy240: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 3; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 5; -#line 130 "src/sfizz/OpcodeCleanup.re" +#line 131 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil", group(1), "_type"); goto end_region; } -#line 1646 "src/sfizz/OpcodeCleanup.cpp" -yy231: +#line 1734 "src/sfizz/OpcodeCleanup.cpp" +yy242: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy256; + case 'o': goto yy270; + default: goto yy137; + } +yy243: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: goto yy271; case '0': case '1': case '2': @@ -1656,38 +1750,38 @@ yy231: case '6': case '7': case '8': - case '9': goto yy231; + case '9': goto yy243; default: goto yy34; } -yy233: +yy245: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy258; + case 'f': goto yy273; default: goto yy34; } -yy234: +yy246: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy259; + case 'e': goto yy274; default: goto yy34; } -yy235: +yy247: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy237; + case 'o': goto yy249; default: goto yy34; } -yy236: +yy248: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy260; + case 'n': goto yy275; default: goto yy34; } -yy237: +yy249: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy261; + if (yych <= 0x00) goto yy276; goto yy34; -yy238: +yy250: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1696,13 +1790,13 @@ yy238: yypmatch[2] = yyt1 - 4; yypmatch[3] = yyt1 - 2; yypmatch[5] = YYCURSOR - 1; -#line 149 "src/sfizz/OpcodeCleanup.re" +#line 155 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("start_", group(1), "cc", group(2)); goto end_region; } -#line 1705 "src/sfizz/OpcodeCleanup.cpp" -yy240: +#line 1799 "src/sfizz/OpcodeCleanup.cpp" +yy252: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1716,99 +1810,111 @@ yy240: case '8': case '9': yyt1 = YYCURSOR; - goto yy263; - default: goto yy34; - } -yy241: - yych = *++YYCURSOR; - switch (yych) { - case '_': goto yy265; - default: goto yy34; - } -yy242: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: - yyt2 = yyt3 = NULL; - goto yy266; - case '_': - yyt3 = YYCURSOR; - goto yy268; - default: goto yy34; - } -yy243: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy270; - default: goto yy34; - } -yy244: - yych = *++YYCURSOR; - switch (yych) { - case 'y': goto yy245; - default: goto yy34; - } -yy245: - yych = *++YYCURSOR; - switch (yych) { - case 'c': goto yy271; - default: goto yy34; - } -yy246: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy272; - default: goto yy34; - } -yy247: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy245; - default: goto yy34; - } -yy248: - yych = *++YYCURSOR; - switch (yych) { - case 'a': goto yy273; - default: goto yy34; - } -yy249: - yych = *++YYCURSOR; - switch (yych) { - case 't': goto yy274; - default: goto yy34; - } -yy250: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy275; - default: goto yy34; - } -yy251: - yych = *++YYCURSOR; - switch (yych) { - case 'q': goto yy275; - default: goto yy34; - } -yy252: - yych = *++YYCURSOR; - switch (yych) { - case 0x00: - yyt4 = yyt5 = NULL; - yyt3 = YYCURSOR; - goto yy276; - case '_': - yyt3 = yyt5 = YYCURSOR; goto yy278; default: goto yy34; } yy253: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy280; + case '_': goto yy280; default: goto yy34; } yy254: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: + yyt2 = yyt3 = NULL; + goto yy281; + case '_': + yyt3 = YYCURSOR; + goto yy283; + default: goto yy34; + } +yy255: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy285; + default: goto yy34; + } +yy256: + yych = *++YYCURSOR; + switch (yych) { + case 'y': goto yy257; + default: goto yy34; + } +yy257: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy286; + default: goto yy34; + } +yy258: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy287; + default: goto yy34; + } +yy259: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy257; + default: goto yy34; + } +yy260: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy288; + default: goto yy34; + } +yy261: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy289; + default: goto yy34; + } +yy262: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy290; + default: goto yy34; + } +yy263: + yych = *++YYCURSOR; + switch (yych) { + case 'q': goto yy290; + default: goto yy34; + } +yy264: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy291; + default: goto yy34; + } +yy265: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy292; + default: goto yy201; + } +yy266: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: + yyt4 = yyt5 = NULL; + yyt3 = YYCURSOR; + goto yy293; + case '_': + yyt3 = yyt5 = YYCURSOR; + goto yy295; + default: goto yy34; + } +yy267: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy297; + default: goto yy34; + } +yy268: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -1819,13 +1925,19 @@ yy254: yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 98 "src/sfizz/OpcodeCleanup.re" +#line 99 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3)); goto end_region; } -#line 1828 "src/sfizz/OpcodeCleanup.cpp" -yy256: +#line 1934 "src/sfizz/OpcodeCleanup.cpp" +yy270: + yych = *++YYCURSOR; + switch (yych) { + case 'm': goto yy298; + default: goto yy137; + } +yy271: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1834,31 +1946,31 @@ yy256: yypmatch[2] = yyt1 - 8; yypmatch[3] = yyt1 - 6; yypmatch[5] = YYCURSOR - 1; -#line 171 "src/sfizz/OpcodeCleanup.re" +#line 182 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "hdcc", group(2)); goto end_region; } -#line 1843 "src/sfizz/OpcodeCleanup.cpp" -yy258: +#line 1955 "src/sfizz/OpcodeCleanup.cpp" +yy273: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy281; + case 'f': goto yy299; default: goto yy34; } -yy259: +yy274: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy237; + case 't': goto yy249; default: goto yy34; } -yy260: +yy275: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy282; + case 'a': goto yy300; default: goto yy34; } -yy261: +yy276: ++YYCURSOR; yynmatch = 3; yypmatch[2] = yyt1; @@ -1867,16 +1979,16 @@ yy261: yypmatch[1] = YYCURSOR; yypmatch[3] = yyt2 - 1; yypmatch[5] = YYCURSOR - 1; -#line 102 "src/sfizz/OpcodeCleanup.re" +#line 103 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "1"); goto end_region; } -#line 1876 "src/sfizz/OpcodeCleanup.cpp" -yy263: +#line 1988 "src/sfizz/OpcodeCleanup.cpp" +yy278: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy283; + case 0x00: goto yy301; case '0': case '1': case '2': @@ -1886,72 +1998,84 @@ yy263: case '6': case '7': case '8': - case '9': goto yy263; + case '9': goto yy278; default: goto yy34; } -yy265: +yy280: yych = *++YYCURSOR; switch (yych) { - case 'g': goto yy285; + case 'g': goto yy303; default: goto yy34; } -yy266: +yy281: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 166 "src/sfizz/OpcodeCleanup.re" +#line 177 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("resonance1", group(1)); goto end_region; } -#line 1911 "src/sfizz/OpcodeCleanup.cpp" -yy268: +#line 2023 "src/sfizz/OpcodeCleanup.cpp" +yy283: yych = *++YYCURSOR; if (yych <= 0x00) { yyt2 = YYCURSOR; - goto yy266; + goto yy281; } - goto yy268; -yy270: + goto yy283; +yy285: yych = *++YYCURSOR; switch (yych) { - case 'k': goto yy245; + case 'k': goto yy257; default: goto yy34; } -yy271: +yy286: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy286; + case 'c': goto yy304; default: goto yy34; } -yy272: +yy287: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy287; + case 's': goto yy305; default: goto yy34; } -yy273: +yy288: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy288; + case 'i': goto yy306; default: goto yy34; } -yy274: +yy289: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy275; + case 'h': goto yy290; default: goto yy34; } -yy275: +yy290: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy289; + case 'c': goto yy307; default: goto yy34; } -yy276: +yy291: + yych = *++YYCURSOR; + switch (yych) { + case 'd': goto yy308; + default: goto yy34; + } +yy292: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy309; + default: goto yy201; + } +yy293: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -1962,44 +2086,48 @@ yy276: yypmatch[0] = yyt1; yypmatch[1] = YYCURSOR; yypmatch[3] = yyt2 - 1; -#line 110 "src/sfizz/OpcodeCleanup.re" +#line 111 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); goto end_region; } -#line 1971 "src/sfizz/OpcodeCleanup.cpp" -yy278: +#line 2095 "src/sfizz/OpcodeCleanup.cpp" +yy295: yych = *++YYCURSOR; if (yych <= 0x00) { yyt4 = YYCURSOR; - goto yy276; + goto yy293; } - goto yy278; -yy280: + goto yy295; +yy297: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy290; + case 'c': goto yy310; default: goto yy34; } -yy281: +yy298: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy311; + goto yy136; +yy299: yych = *++YYCURSOR; switch (yych) { case 0x00: yyt4 = yyt5 = NULL; yyt3 = YYCURSOR; - goto yy291; + goto yy313; case '_': yyt3 = yyt5 = YYCURSOR; - goto yy293; + goto yy315; default: goto yy34; } -yy282: +yy300: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy295; + case 'n': goto yy317; default: goto yy34; } -yy283: +yy301: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -2008,19 +2136,19 @@ yy283: yypmatch[2] = yyt1 - 6; yypmatch[3] = yyt1 - 4; yypmatch[5] = YYCURSOR - 1; -#line 153 "src/sfizz/OpcodeCleanup.re" +#line 159 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("start_", group(1), "hdcc", group(2)); goto end_region; } -#line 2017 "src/sfizz/OpcodeCleanup.cpp" -yy285: +#line 2145 "src/sfizz/OpcodeCleanup.cpp" +yy303: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy296; + case 'r': goto yy318; default: goto yy34; } -yy286: +yy304: yych = *++YYCURSOR; switch (yych) { case '0': @@ -2034,34 +2162,57 @@ yy286: case '8': case '9': yyt3 = YYCURSOR; - goto yy297; + goto yy319; default: goto yy34; } -yy287: +yy305: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy245; + case 'e': goto yy257; default: goto yy34; } -yy288: +yy306: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy245; + case 'n': goto yy257; default: goto yy34; } -yy289: +yy307: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy299; + case 'c': goto yy321; default: goto yy34; } -yy290: +yy308: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy252; + case 'o': goto yy322; default: goto yy34; } -yy291: +yy309: + yych = *++YYCURSOR; + switch (yych) { + case 'm': goto yy323; + default: goto yy201; + } +yy310: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy266; + default: goto yy34; + } +yy311: + ++YYCURSOR; + yynmatch = 1; + yypmatch[0] = YYCURSOR - 12; + yypmatch[1] = YYCURSOR; +#line 141 "src/sfizz/OpcodeCleanup.re" + { + opcode = "amp_random"; + goto end_region; + } +#line 2215 "src/sfizz/OpcodeCleanup.cpp" +yy313: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -2072,35 +2223,35 @@ yy291: yypmatch[0] = yyt1; yypmatch[1] = YYCURSOR; yypmatch[3] = yyt2 - 1; -#line 106 "src/sfizz/OpcodeCleanup.re" +#line 107 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); goto end_region; } -#line 2081 "src/sfizz/OpcodeCleanup.cpp" -yy293: +#line 2232 "src/sfizz/OpcodeCleanup.cpp" +yy315: yych = *++YYCURSOR; if (yych <= 0x00) { yyt4 = YYCURSOR; - goto yy291; + goto yy313; } - goto yy293; -yy295: + goto yy315; +yy317: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy300; + case 'c': goto yy324; default: goto yy34; } -yy296: +yy318: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy301; + case 'o': goto yy325; default: goto yy34; } -yy297: +yy319: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy302; + case 0x00: goto yy326; case '0': case '1': case '2': @@ -2110,10 +2261,10 @@ yy297: case '6': case '7': case '8': - case '9': goto yy297; + case '9': goto yy319; default: goto yy34; } -yy299: +yy321: yych = *++YYCURSOR; switch (yych) { case '0': @@ -2127,22 +2278,32 @@ yy299: case '8': case '9': yyt3 = YYCURSOR; - goto yy304; + goto yy328; default: goto yy34; } -yy300: +yy322: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy281; + case 'm': goto yy330; default: goto yy34; } -yy301: +yy323: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy331; + goto yy200; +yy324: yych = *++YYCURSOR; switch (yych) { - case 'u': goto yy306; + case 'e': goto yy299; default: goto yy34; } -yy302: +yy325: + yych = *++YYCURSOR; + switch (yych) { + case 'u': goto yy333; + default: goto yy34; + } +yy326: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -2153,16 +2314,16 @@ yy302: yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 94 "src/sfizz/OpcodeCleanup.re" +#line 95 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3)); goto end_region; } -#line 2162 "src/sfizz/OpcodeCleanup.cpp" -yy304: +#line 2323 "src/sfizz/OpcodeCleanup.cpp" +yy328: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy307; + case 0x00: goto yy334; case '0': case '1': case '2': @@ -2172,16 +2333,32 @@ yy304: case '6': case '7': case '8': - case '9': goto yy304; + case '9': goto yy328; default: goto yy34; } -yy306: +yy330: + yych = *++YYCURSOR; + if (yych >= 0x01) goto yy34; +yy331: + ++YYCURSOR; + yynmatch = 2; + yypmatch[0] = yyt1; + yypmatch[2] = yyt2; + yypmatch[3] = yyt4; + yypmatch[1] = YYCURSOR; +#line 164 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat("fil", group(1), "_random"); + goto again_region; + } +#line 2355 "src/sfizz/OpcodeCleanup.cpp" +yy333: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy309; + case 'p': goto yy336; default: goto yy34; } -yy307: +yy334: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -2192,27 +2369,27 @@ yy307: yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 90 "src/sfizz/OpcodeCleanup.re" +#line 91 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3)); goto end_region; } -#line 2201 "src/sfizz/OpcodeCleanup.cpp" -yy309: +#line 2378 "src/sfizz/OpcodeCleanup.cpp" +yy336: yych = *++YYCURSOR; if (yych >= 0x01) goto yy34; ++YYCURSOR; yynmatch = 1; yypmatch[0] = YYCURSOR - 16; yypmatch[1] = YYCURSOR; -#line 135 "src/sfizz/OpcodeCleanup.re" +#line 136 "src/sfizz/OpcodeCleanup.re" { opcode = "group"; goto end_region; } -#line 2214 "src/sfizz/OpcodeCleanup.cpp" +#line 2391 "src/sfizz/OpcodeCleanup.cpp" } -#line 180 "src/sfizz/OpcodeCleanup.re" +#line 191 "src/sfizz/OpcodeCleanup.re" end_region: @@ -2227,80 +2404,80 @@ end_region: YYCURSOR = opcode.c_str(); -#line 2231 "src/sfizz/OpcodeCleanup.cpp" +#line 2408 "src/sfizz/OpcodeCleanup.cpp" { char yych; yych = *YYCURSOR; switch (yych) { - case 's': goto yy316; - default: goto yy314; + case 's': goto yy343; + default: goto yy341; } -yy314: +yy341: ++YYCURSOR; -yy315: -#line 200 "src/sfizz/OpcodeCleanup.re" +yy342: +#line 211 "src/sfizz/OpcodeCleanup.re" { goto end_control; } -#line 2246 "src/sfizz/OpcodeCleanup.cpp" -yy316: +#line 2423 "src/sfizz/OpcodeCleanup.cpp" +yy343: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'e': goto yy317; - default: goto yy315; + case 'e': goto yy344; + default: goto yy342; } -yy317: +yy344: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy319; - default: goto yy318; + case 't': goto yy346; + default: goto yy345; } -yy318: +yy345: YYCURSOR = YYMARKER; - goto yy315; -yy319: + goto yy342; +yy346: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy320; - default: goto yy318; + case '_': goto yy347; + default: goto yy345; } -yy320: +yy347: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy321; - default: goto yy318; + case 'r': goto yy348; + default: goto yy345; } -yy321: +yy348: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy322; - default: goto yy318; + case 'e': goto yy349; + default: goto yy345; } -yy322: +yy349: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy323; - default: goto yy318; + case 'a': goto yy350; + default: goto yy345; } -yy323: +yy350: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy324; - default: goto yy318; + case 'l': goto yy351; + default: goto yy345; } -yy324: +yy351: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy325; - default: goto yy318; + case 'c': goto yy352; + default: goto yy345; } -yy325: +yy352: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy326; - default: goto yy318; + case 'c': goto yy353; + default: goto yy345; } -yy326: +yy353: yych = *++YYCURSOR; switch (yych) { case '0': @@ -2314,13 +2491,13 @@ yy326: case '8': case '9': yyt1 = YYCURSOR; - goto yy327; - default: goto yy318; + goto yy354; + default: goto yy345; } -yy327: +yy354: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy329; + case 0x00: goto yy356; case '0': case '1': case '2': @@ -2330,24 +2507,24 @@ yy327: case '6': case '7': case '8': - case '9': goto yy327; - default: goto yy318; + case '9': goto yy354; + default: goto yy345; } -yy329: +yy356: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 10; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 195 "src/sfizz/OpcodeCleanup.re" +#line 206 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("set_hdcc", group(1)); goto end_control; } -#line 2349 "src/sfizz/OpcodeCleanup.cpp" +#line 2526 "src/sfizz/OpcodeCleanup.cpp" } -#line 204 "src/sfizz/OpcodeCleanup.re" +#line 215 "src/sfizz/OpcodeCleanup.re" end_control: diff --git a/src/sfizz/OpcodeCleanup.re b/src/sfizz/OpcodeCleanup.re index c744d4ed..245f62d5 100644 --- a/src/sfizz/OpcodeCleanup.re +++ b/src/sfizz/OpcodeCleanup.re @@ -76,6 +76,7 @@ end_region_oncc: //-------------------------------------------------------------------------- if (scope == kOpcodeScopeRegion) { + again_region: YYCURSOR = opcode.c_str(); @@ -137,6 +138,11 @@ end_region_oncc: goto end_region; } + "gain_random" END { + opcode = "amp_random"; + goto end_region; + } + "gain" ("_" any)? END { opcode = absl::StrCat("volume", group(1)); goto end_region; @@ -155,6 +161,11 @@ end_region_oncc: goto end_region; } + "cutoff" (number)? "_random" END { + opcode = absl::StrCat("fil", group(1), "_random"); + goto again_region; + } + "fil_" (any) END { opcode = absl::StrCat("fil1_", group(1)); goto end_region; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index c599bc66..2bb808aa 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -654,7 +654,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, filters[filterIndex].veltrack, Default::filterVeltrackRange); } break; - case hash("fil&_random"): // also fil_random + case hash("fil&_random"): // also fil_random, cutoff_random, cutoff&_random { const auto filterIndex = opcode.parameters.front() - 1; if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) diff --git a/tests/OpcodeT.cpp b/tests/OpcodeT.cpp index 771db749..495a4f5f 100644 --- a/tests/OpcodeT.cpp +++ b/tests/OpcodeT.cpp @@ -233,6 +233,11 @@ TEST_CASE("[Opcode] Normalization") {"cutoff_foobar", "cutoff1_foobar"}, {"resonance", "resonance1"}, {"resonance_foobar", "resonance1_foobar"}, + // Cakewalk aliases + {"cutoff_random", "fil1_random"}, + {"cutoff1_random", "fil1_random"}, + {"cutoff2_random", "fil2_random"}, + {"gain_random", "amp_random"}, }; for (auto pair : regionSpecific) { From a20a4c0029506763f9dbfcc7cdd5e860b99dcafd Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 27 Sep 2020 11:25:43 +0200 Subject: [PATCH 334/445] flexEG: If stage is of zero duration, immediate transition to level --- src/sfizz/FlexEnvelope.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index 70dc0948..c58d3ae5 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -144,6 +144,9 @@ void FlexEnvelope::Impl::process(absl::Span out) const bool isReleased = isReleased_; while ((!stageSustained_ && currentTime_ >= stageTime_) || (stageSustained_ && isReleased)) { + // If stage is of zero duration, immediate transition to level + if (!stageSustained_ && stageTime_ == 0) + currentLevel_ = stageTargetLevel_; if (!advanceToNextStage()) { out.remove_prefix(frameIndex); fill(out, 0.0f); From 6694e8b85e45ff9cdc4a8639990f6e27b1d3d38e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 28 Sep 2020 14:43:58 +0200 Subject: [PATCH 335/445] Add test --- tests/FlexEGT.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp index db16f93c..229bee47 100644 --- a/tests/FlexEGT.cpp +++ b/tests/FlexEGT.cpp @@ -9,6 +9,7 @@ #include "sfizz/FlexEnvelope.h" #include "catch2/catch.hpp" #include "TestHelpers.h" +#include using namespace Catch::literals; using namespace sfz::literals; @@ -261,3 +262,29 @@ TEST_CASE("[FlexEG] Detailed numerical envelope test (with shapes)") envelope.process(absl::MakeSpan(output)); REQUIRE( approxEqual(output, expected, 0.01f) ); } + +TEST_CASE("[FlexEG] Zero delay transitions") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=0 eg1_level1=1 + eg1_time2=1 eg1_level2=0 + eg1_time3=1 eg1_level3=.5 eg1_sustain=3 + eg1_time4=1 eg1_level4=1 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE(synth.getRegionView(0)->flexEGs.size() == 1); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + envelope.setSampleRate(10); + envelope.start(1); + + std::array output; + envelope.process(absl::MakeSpan(output)); + REQUIRE(output[0] == 0.0f); + REQUIRE(output[1] == Approx(0.9f).margin(0.01f)); + // Note(jpc): 0.9 is because EG pre-increments the time counter, slope is + // 1 frame off into the future +} From 3412ead0ac25e47ac2e48b1cc7592b678f976f32 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 26 Sep 2020 14:51:43 +0200 Subject: [PATCH 336/445] Ampeg in modulation matrix --- src/sfizz/Synth.cpp | 6 ++++++ src/sfizz/Voice.cpp | 11 ++++------- src/sfizz/Voice.h | 4 ++++ src/sfizz/modulations/ModId.cpp | 2 ++ src/sfizz/modulations/ModId.h | 1 + src/sfizz/modulations/ModKey.cpp | 2 ++ src/sfizz/modulations/sources/ADSREnvelope.cpp | 13 +++++++++++++ 7 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b191d841..52b02c96 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -159,6 +159,11 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) ModKey::createCC(10, 1, defaultSmoothness, 100, 0), ModKey::createNXYZ(ModId::Pan, lastRegion->id)).sourceDepth = 1.0f; + // Create the amplitude envelope + lastRegion->getOrCreateConnection( + ModKey::createNXYZ(ModId::AmpEG, lastRegion->id), + ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 100.0f; + // auto parseOpcodes = [&](const std::vector& opcodes) { for (auto& opcode : opcodes) { @@ -1530,6 +1535,7 @@ void sfz::Synth::setupModMatrix() case ModId::Envelope: gen = genFlexEnvelope.get(); break; + case ModId::AmpEG: case ModId::PitchEG: case ModId::FilEG: gen = genADSREnvelope.get(); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a60b1af7..98ecf495 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -130,7 +130,6 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event bendStepFactor = centsFactor(region->bendStep); bendSmoother.setSmoothing(region->bendSmooth, sampleRate); bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); - egAmplitude.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate); resources.modMatrix.initVoice(id, region->getId(), delay); saveModulationTargets(region); @@ -154,8 +153,6 @@ void sfz::Voice::release(int delay) noexcept if (egAmplitude.getRemainingDelay() > delay) { switchState(State::cleanMeUp); - } else { - egAmplitude.startRelease(delay); } resources.modMatrix.releaseVoice(id, region->getId(), delay); @@ -367,14 +364,14 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept ModMatrix& mm = resources.modMatrix; - // AmpEG envelope - egAmplitude.getBlock(modulationSpan); - // Amplitude envelope applyGain1(baseGain, modulationSpan); if (float* mod = mm.getModulation(amplitudeTarget)) { for (size_t i = 0; i < numSamples; ++i) - modulationSpan[i] *= normalizePercents(mod[i]); + modulationSpan[i] = normalizePercents(mod[i]); + } + else { + ASSERTFALSE; } // Volume envelope diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 419c6584..86b1c5ab 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -336,6 +336,10 @@ public: Duration getLastFilterDuration() const noexcept { return filterDuration; } Duration getLastPanningDuration() const noexcept { return panningDuration; } + /** + * @brief Get the SFZv1 amplitude EG, if existing + */ + ADSREnvelope* getAmplitudeEG() { return &egAmplitude; } /** * @brief Get the SFZv1 pitch EG, if existing */ diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index 427d7511..d21ba9e0 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -30,6 +30,8 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice; case ModId::LFO: return kModIsPerVoice; + case ModId::AmpEG: + return kModIsPerVoice; case ModId::PitchEG: return kModIsPerVoice; case ModId::FilEG: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index ccd25b98..3eb7022d 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -23,6 +23,7 @@ enum class ModId : int { Controller = _SourcesStart, Envelope, LFO, + AmpEG, PitchEG, FilEG, diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 80c3accb..2b9767d3 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -74,6 +74,8 @@ std::string ModKey::toString() const return absl::StrCat("EG ", 1 + params_.N, " {", region_.number(), "}"); case ModId::LFO: return absl::StrCat("LFO ", 1 + params_.N, " {", region_.number(), "}"); + case ModId::AmpEG: + return absl::StrCat("AmplitudeEG {", region_.number(), "}"); case ModId::PitchEG: return absl::StrCat("PitchEG {", region_.number(), "}"); case ModId::FilEG: diff --git a/src/sfizz/modulations/sources/ADSREnvelope.cpp b/src/sfizz/modulations/sources/ADSREnvelope.cpp index 7128b216..57f8ba7d 100644 --- a/src/sfizz/modulations/sources/ADSREnvelope.cpp +++ b/src/sfizz/modulations/sources/ADSREnvelope.cpp @@ -35,6 +35,11 @@ void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, const EGDescription* desc = nullptr; switch (sourceKey.id()) { + case ModId::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + desc = ®ion->amplitudeEG; + break; case ModId::PitchEG: eg = voice->getPitchEG(); ASSERT(eg); @@ -69,6 +74,10 @@ void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId voice ADSREnvelope* eg = nullptr; switch (sourceKey.id()) { + case ModId::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + break; case ModId::PitchEG: eg = voice->getPitchEG(); ASSERT(eg); @@ -98,6 +107,10 @@ void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId voic ADSREnvelope* eg = nullptr; switch (sourceKey.id()) { + case ModId::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + break; case ModId::PitchEG: eg = voice->getPitchEG(); ASSERT(eg); From c30cad25340e58f499d4a268351ee4f9c2f26090 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 26 Sep 2020 15:54:41 +0200 Subject: [PATCH 337/445] Implement egN_ampeg --- src/sfizz/FlexEGDescription.h | 2 ++ src/sfizz/FlexEnvelope.cpp | 19 ++++++++++++++ src/sfizz/FlexEnvelope.h | 15 +++++++++++ src/sfizz/Region.cpp | 33 ++++++++++++++++++++--- src/sfizz/Region.h | 2 ++ src/sfizz/Synth.cpp | 18 +++++++++---- src/sfizz/Voice.cpp | 49 +++++++++++++++++++++++++++-------- 7 files changed, 119 insertions(+), 19 deletions(-) diff --git a/src/sfizz/FlexEGDescription.h b/src/sfizz/FlexEGDescription.h index fd8e9428..b8a0f59b 100644 --- a/src/sfizz/FlexEGDescription.h +++ b/src/sfizz/FlexEGDescription.h @@ -34,6 +34,8 @@ struct FlexEGDescription { int dynamic { Default::flexEGDynamic }; // whether parameters can be modulated while EG runs int sustain { Default::flexEGSustain }; // index of the sustain point (default to 0 in ARIA) std::vector points; + // ARIA + bool ampeg = false; // replaces the SFZv1 AmpEG (lowest with this bit wins) }; } // namespace sfz diff --git a/src/sfizz/FlexEnvelope.cpp b/src/sfizz/FlexEnvelope.cpp index c58d3ae5..c9302c0b 100644 --- a/src/sfizz/FlexEnvelope.cpp +++ b/src/sfizz/FlexEnvelope.cpp @@ -104,6 +104,25 @@ void FlexEnvelope::release(unsigned releaseDelay) impl.currentFramesUntilRelease_ = releaseDelay; } +unsigned FlexEnvelope::getRemainingDelay() const noexcept +{ + const Impl& impl = *impl_; + return static_cast(impl.delayFramesLeft_); +} + +bool FlexEnvelope::isReleased() const noexcept +{ + const Impl& impl = *impl_; + return impl.isReleased_; +} + +bool FlexEnvelope::isFinished() const noexcept +{ + const Impl& impl = *impl_; + const FlexEGDescription& desc = *impl.desc_; + return impl.currentStageNumber_ >= desc.points.size(); +} + void FlexEnvelope::process(absl::Span out) { Impl& impl = *impl_; diff --git a/src/sfizz/FlexEnvelope.h b/src/sfizz/FlexEnvelope.h index 3316e715..d1d01a4a 100644 --- a/src/sfizz/FlexEnvelope.h +++ b/src/sfizz/FlexEnvelope.h @@ -40,6 +40,21 @@ public: */ void release(unsigned releaseDelay); + /** + Get the remaining delay samples + */ + unsigned getRemainingDelay() const noexcept; + + /** + Is the envelope released? + */ + bool isReleased() const noexcept; + + /** + Is the envelope finished? + */ + bool isFinished() const noexcept; + /** Process a cycle of the generator. */ diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index c599bc66..b48ed7f3 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1162,6 +1162,28 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqBandwidth, Default::eqBandwidthModRange); break; + case hash("eg&_ampeg"): + { + const auto egNumber = opcode.parameters.front(); + if (egNumber == 0) + return false; + if (!extendIfNecessary(flexEGs, egNumber, Default::numFlexEGs)) + return false; + if (auto value = readOpcode(opcode.value, Range { 0, 1 })) { + FlexEGDescription& desc = flexEGs[egNumber - 1]; + bool ampeg = *value != 0; + if (desc.ampeg != ampeg) { + desc.ampeg = ampeg; + flexAmpEG = absl::nullopt; + for (size_t i = 0, n = flexEGs.size(); i < n && !flexAmpEG; ++i) { + if (flexEGs[i].ampeg) + flexAmpEG = static_cast(i); + } + } + } + break; + } + // Amplitude Envelope case hash("ampeg_attack"): case hash("ampeg_decay"): @@ -1907,7 +1929,7 @@ float sfz::Region::getBendInCents(float bend) const noexcept return bend > 0.0f ? bend * static_cast(bendUp) : -bend * static_cast(bendDown); } -sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source, const ModKey& target) +sfz::Region::Connection* sfz::Region::getConnection(const ModKey& source, const ModKey& target) { auto pred = [&source, &target](const Connection& c) { @@ -1915,8 +1937,13 @@ sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source }; auto it = std::find_if(connections.begin(), connections.end(), pred); - if (it != connections.end()) - return *it; + return (it == connections.end()) ? nullptr : &*it; +} + +sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source, const ModKey& target) +{ + if (Connection* c = getConnection(source, target)) + return *c; sfz::Region::Connection c; c.source = source; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 76840c53..62692437 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -425,6 +425,7 @@ struct Region { // Envelopes std::vector flexEGs; + absl::optional flexAmpEG; // egN_ampeg // LFOs std::vector lfos; @@ -445,6 +446,7 @@ struct Region { float velToDepth = 0.0f; }; std::vector connections; + Connection* getConnection(const ModKey& source, const ModKey& target); Connection& getOrCreateConnection(const ModKey& source, const ModKey& target); // Parent diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 52b02c96..4e7a9f7d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -159,11 +159,6 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) ModKey::createCC(10, 1, defaultSmoothness, 100, 0), ModKey::createNXYZ(ModId::Pan, lastRegion->id)).sourceDepth = 1.0f; - // Create the amplitude envelope - lastRegion->getOrCreateConnection( - ModKey::createNXYZ(ModId::AmpEG, lastRegion->id), - ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 100.0f; - // auto parseOpcodes = [&](const std::vector& opcodes) { for (auto& opcode : opcodes) { @@ -182,6 +177,19 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) parseOpcodes(groupOpcodes); parseOpcodes(regionOpcodes); + // Create the amplitude envelope + if (!lastRegion->flexAmpEG) { + lastRegion->getOrCreateConnection( + ModKey::createNXYZ(ModId::AmpEG, lastRegion->id), + ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 100.0f; + } + else { + ModKey source = ModKey::createNXYZ(ModId::Envelope, lastRegion->id, *lastRegion->flexAmpEG); + ModKey target = ModKey::createNXYZ(ModId::Amplitude, lastRegion->id); + if (!lastRegion->getConnection(source, target)) + lastRegion->getOrCreateConnection(source, target).sourceDepth = 100.0f; + } + if (octaveOffset != 0 || noteOffset != 0) lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 98ecf495..ebed105b 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -151,8 +151,13 @@ void sfz::Voice::release(int delay) noexcept if (state != State::playing) return; - if (egAmplitude.getRemainingDelay() > delay) { - switchState(State::cleanMeUp); + if (!region->flexAmpEG) { + if (egAmplitude.getRemainingDelay() > delay) + switchState(State::cleanMeUp); + } + else { + if (flexEGs[*region->flexAmpEG]->getRemainingDelay() > static_cast(delay)) + switchState(State::cleanMeUp); } resources.modMatrix.releaseVoice(id, region->getId(), delay); @@ -160,10 +165,15 @@ void sfz::Voice::release(int delay) noexcept void sfz::Voice::off(int delay) noexcept { - if (region->offMode == SfzOffMode::fast) { - egAmplitude.setReleaseTime( Default::offTime ); - } else if (region->offMode == SfzOffMode::time) { - egAmplitude.setReleaseTime(region->offTime); + if (!region->flexAmpEG) { + if (region->offMode == SfzOffMode::fast) { + egAmplitude.setReleaseTime( Default::offTime ); + } else if (region->offMode == SfzOffMode::time) { + egAmplitude.setReleaseTime(region->offTime); + } + } + else { + // TODO(jpc): Flex AmpEG } release(delay); @@ -283,8 +293,14 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept panStageMono(buffer); } - if (!egAmplitude.isSmoothing()) - switchState(State::cleanMeUp); + if (!region->flexAmpEG) { + if (!egAmplitude.isSmoothing()) + switchState(State::cleanMeUp); + } + else { + if (flexEGs[*region->flexAmpEG]->isFinished()) + switchState(State::cleanMeUp); + } powerFollower.process(buffer); @@ -569,8 +585,14 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept << " for sample " << region->sampleId); } #endif - egAmplitude.setReleaseTime(0.0f); - egAmplitude.startRelease(i); + if (!region->flexAmpEG) { + egAmplitude.setReleaseTime(0.0f); + egAmplitude.startRelease(i); + } + else { + // TODO(jpc): Flex AmpEG + flexEGs[*region->flexAmpEG]->release(i); + } fill(indices->subspan(i), sampleEnd); fill(coeffs->subspan(i), 1.0f); break; @@ -850,7 +872,12 @@ float sfz::Voice::getAveragePower() const noexcept bool sfz::Voice::releasedOrFree() const noexcept { - return state != State::playing || egAmplitude.isReleased(); + if (state != State::playing) + return true; + if (!region->flexAmpEG) + return egAmplitude.isReleased(); + else + return flexEGs[*region->flexAmpEG]->isReleased(); } uint32_t sfz::Voice::getSourcePosition() const noexcept From 2d3f82331d5cb2fb2e8884d2fb9988f17fb3c451 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 26 Sep 2020 17:10:02 +0200 Subject: [PATCH 338/445] Allow egN_ampeg and egN_amplitude both together --- src/sfizz/Synth.cpp | 15 ++++++--------- src/sfizz/Voice.cpp | 11 +++++++---- src/sfizz/Voice.h | 1 + src/sfizz/modulations/ModId.cpp | 2 ++ src/sfizz/modulations/ModId.h | 3 ++- src/sfizz/modulations/ModKey.cpp | 2 ++ 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 4e7a9f7d..53f5c944 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -178,17 +178,14 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) parseOpcodes(regionOpcodes); // Create the amplitude envelope - if (!lastRegion->flexAmpEG) { + if (!lastRegion->flexAmpEG) lastRegion->getOrCreateConnection( ModKey::createNXYZ(ModId::AmpEG, lastRegion->id), - ModKey::createNXYZ(ModId::Amplitude, lastRegion->id)).sourceDepth = 100.0f; - } - else { - ModKey source = ModKey::createNXYZ(ModId::Envelope, lastRegion->id, *lastRegion->flexAmpEG); - ModKey target = ModKey::createNXYZ(ModId::Amplitude, lastRegion->id); - if (!lastRegion->getConnection(source, target)) - lastRegion->getOrCreateConnection(source, target).sourceDepth = 100.0f; - } + ModKey::createNXYZ(ModId::MasterAmplitude, lastRegion->id)).sourceDepth = 1.0f; + else + lastRegion->getOrCreateConnection( + ModKey::createNXYZ(ModId::Envelope, lastRegion->id, *lastRegion->flexAmpEG), + ModKey::createNXYZ(ModId::MasterAmplitude, lastRegion->id)).sourceDepth = 1.0f; if (octaveOffset != 0 || noteOffset != 0) lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index ebed105b..ffb15f75 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -380,14 +380,16 @@ void sfz::Voice::amplitudeEnvelope(absl::Span modulationSpan) noexcept ModMatrix& mm = resources.modMatrix; + // Amplitude EG + absl::Span ampegOut(mm.getModulation(masterAmplitudeTarget), numSamples); + ASSERT(ampegOut.data()); + copy(ampegOut, modulationSpan); + // Amplitude envelope applyGain1(baseGain, modulationSpan); if (float* mod = mm.getModulation(amplitudeTarget)) { for (size_t i = 0; i < numSamples; ++i) - modulationSpan[i] = normalizePercents(mod[i]); - } - else { - ASSERTFALSE; + modulationSpan[i] *= normalizePercents(mod[i]); } // Volume envelope @@ -1050,6 +1052,7 @@ void sfz::Voice::resetSmoothers() noexcept void sfz::Voice::saveModulationTargets(const Region* region) noexcept { ModMatrix& mm = resources.modMatrix; + masterAmplitudeTarget = mm.findTarget(ModKey::createNXYZ(ModId::MasterAmplitude, region->getId())); amplitudeTarget = mm.findTarget(ModKey::createNXYZ(ModId::Amplitude, region->getId())); volumeTarget = mm.findTarget(ModKey::createNXYZ(ModId::Volume, region->getId())); panTarget = mm.findTarget(ModKey::createNXYZ(ModId::Pan, region->getId())); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 86b1c5ab..aa49297c 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -520,6 +520,7 @@ private: Smoother xfadeSmoother; void resetSmoothers() noexcept; + ModMatrix::TargetId masterAmplitudeTarget; ModMatrix::TargetId amplitudeTarget; ModMatrix::TargetId volumeTarget; ModMatrix::TargetId panTarget; diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index d21ba9e0..3d837ec5 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -38,6 +38,8 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice; // targets + case ModId::MasterAmplitude: + return kModIsPerVoice|kModIsPercentMultiplicative; case ModId::Amplitude: return kModIsPerVoice|kModIsPercentMultiplicative; case ModId::Pan: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index 3eb7022d..48d9b2d9 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -34,7 +34,8 @@ enum class ModId : int { //-------------------------------------------------------------------------- _TargetsStart = _SourcesEnd, - Amplitude = _TargetsStart, + MasterAmplitude = _TargetsStart, + Amplitude, Pan, Width, Position, diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 2b9767d3..5fa48181 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -81,6 +81,8 @@ std::string ModKey::toString() const case ModId::FilEG: return absl::StrCat("FilterEG {", region_.number(), "}"); + case ModId::MasterAmplitude: + return absl::StrCat("MasterAmplitude {", region_.number(), "}"); case ModId::Amplitude: return absl::StrCat("Amplitude {", region_.number(), "}"); case ModId::Pan: From 1afc987794f7d0adb58494578300db373fdf39b0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 26 Sep 2020 17:29:40 +0200 Subject: [PATCH 339/445] Update tests --- tests/TestHelpers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index b3521c4a..6e1fee3f 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -72,6 +72,9 @@ unsigned numPlayingVoices(const sfz::Synth& synth) std::string createReferenceGraph(std::vector lines, int numRegions) { for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { + lines.push_back(absl::StrCat( + R"("AmplitudeEG {)", regionIdx, R"(}" -> "MasterAmplitude {)", regionIdx, R"(}")" + )); lines.push_back(absl::StrCat( R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {)", regionIdx, From 7bd73b893992be9fe368831567f8b121d916ea5d Mon Sep 17 00:00:00 2001 From: redtide Date: Mon, 28 Sep 2020 17:03:37 +0200 Subject: [PATCH 340/445] (Re)moved Doxygen scripts --- doxygen/layout/DoxygenLayout.xml | 187 ----------------- doxygen/layout/custom_footer.html | 12 -- doxygen/layout/custom_header.html | 155 -------------- doxygen/layout/extra_stylesheet.css | 198 ------------------ doxygen/pages/engine_description.md | 154 -------------- doxygen/pages/index.md | 6 - doxygen/scripts/generate_api_index.sh | 19 -- .../scripts => scripts/doxygen}/Doxyfile.in | 0 scripts/doxygen/doxy2json.py | 185 ++++++++++++++++ src/CMakeLists.txt | 2 +- 10 files changed, 186 insertions(+), 732 deletions(-) delete mode 100644 doxygen/layout/DoxygenLayout.xml delete mode 100644 doxygen/layout/custom_footer.html delete mode 100644 doxygen/layout/custom_header.html delete mode 100644 doxygen/layout/extra_stylesheet.css delete mode 100644 doxygen/pages/engine_description.md delete mode 100644 doxygen/pages/index.md delete mode 100755 doxygen/scripts/generate_api_index.sh rename {doxygen/scripts => scripts/doxygen}/Doxyfile.in (100%) create mode 100644 scripts/doxygen/doxy2json.py diff --git a/doxygen/layout/DoxygenLayout.xml b/doxygen/layout/DoxygenLayout.xml deleted file mode 100644 index 48237226..00000000 --- a/doxygen/layout/DoxygenLayout.xml +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doxygen/layout/custom_footer.html b/doxygen/layout/custom_footer.html deleted file mode 100644 index 6f98db55..00000000 --- a/doxygen/layout/custom_footer.html +++ /dev/null @@ -1,12 +0,0 @@ -