diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..1a8485883 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +.* export-ignore +.*/** export-ignore +appveyor.yml export-ignore diff --git a/.gitignore b/.gitignore index 8f4dc499d..d9ddf04ea 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/.gitmodules b/.gitmodules index 1ed527740..1247ca5fe 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"] @@ -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 8b92e35a1..77731c0e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,8 +40,33 @@ jobs: install: .travis/download_cmake.sh script: .travis/script_test.sh - - name: "Windows mingw32" + - name: "macOS" stage: "Build" + os: osx + osx_image: xcode11.3 + addons: + homebrew: + packages: + - cmake + - libsndfile + - jack + - dylibbundler + env: + - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-${TRAVIS_OS_NAME}-${TRAVIS_CPU_ARCH}" + 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 @@ -92,6 +117,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: @@ -107,6 +133,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: @@ -127,28 +154,9 @@ 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) + if: (tag =~ /^v?[0-9]/) AND (type = push) env: - INSTALL_DIR="sfizz-${TRAVIS_BRANCH}-src" addons: @@ -158,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/install_osx.sh b/.travis/install_osx.sh deleted file mode 100755 index d69b82494..000000000 --- a/.travis/install_osx.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -ex - -sudo ln -s /usr/local /opt/local -brew update -brew upgrade cmake -brew install python || brew link --overwrite python -brew install jack -brew install dylibbundler diff --git a/.travis/prepare_osx.sh b/.travis/prepare_osx.sh index 8fc891c93..8de6d08ea 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/.travis/script_library.sh b/.travis/script_library.sh index 0d014f4e6..20449edc4 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 2f1f545f5..85aac04ea 100755 --- a/.travis/script_mingw.sh +++ b/.travis/script_mingw.sh @@ -9,17 +9,17 @@ 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) + buildenv make -j2 elif [[ ${CROSS_COMPILE} == "mingw64" ]]; then buildenv x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release \ -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) + buildenv make -j2 fi diff --git a/.travis/script_moddevices.sh b/.travis/script_moddevices.sh index d36b982c4..3654590d8 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 .. -buildenv mod-plugin-builder make -j + -DCMAKE_BUILD_TYPE=Release -DSFIZZ_JACK=OFF -DSFIZZ_LV2_UI=OFF .. +buildenv mod-plugin-builder make -j2 diff --git a/.travis/script_plugins.sh b/.travis/script_plugins.sh index 89eaf8ac4..5f84a42c3 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) +make -j2 diff --git a/.travis/script_test.sh b/.travis/script_test.sh index 844ec9b9e..8481cbfb4 100755 --- a/.travis/script_test.sh +++ b/.travis/script_test.sh @@ -6,9 +6,9 @@ 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 \ .. -make -j$(nproc) sfizz_tests +make -j2 sfizz_tests tests/sfizz_tests diff --git a/.travis/update_dox.sh b/.travis/update_dox.sh deleted file mode 100755 index 55ea08f4c..000000000 --- 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} diff --git a/AUTHORS.md b/AUTHORS.md index 203180992..4a97b0ed2 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -8,3 +8,5 @@ Contributors to `sfizz`, in chronologic order: - Michael Willis (2020) - Jean-Pierre Cimalando (2020) - Tobiasz "unfa" Karoń (2020) +- Kinwie (2020) +- Atsushi Eno (2020) diff --git a/CMakeLists.txt b/CMakeLists.txt index 205671ac8..dcd2f9b81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,27 +8,21 @@ else() endif() endif() -project (sfizz VERSION 0.4.0 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 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]") -# 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,9 +30,11 @@ 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) +include (SfizzConfig) + # Don't use IPO in non Release builds include (CheckIPO) @@ -51,6 +47,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/GOVERNANCE.md b/GOVERNANCE.md index 3fc7d31c7..d809b00cf 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 provided that this notice +is preserved. This file is offered as-is, without any warranty. + [Open-source Open Collective]: https://opencollective.com/sfztools diff --git a/LICENSE.md b/LICENSE.md index 25c59de3e..d3f769aef 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. diff --git a/README.md b/README.md index 4bb0e1e6b..d55e2f0ef 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 diff --git a/appveyor.yml b/appveyor.yml index d9dd9573b..ea480c915 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 @@ -12,20 +12,22 @@ 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: - 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 after_build: -- cmd: cp sfizz.lv2/Release/sfizz.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/benchmarks/BM_allWithin.cpp b/benchmarks/BM_allWithin.cpp new file mode 100644 index 000000000..c1b3bec03 --- /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/BM_audioReaders.cpp b/benchmarks/BM_audioReaders.cpp new file mode 100644 index 000000000..569a88101 --- /dev/null +++ b/benchmarks/BM_audioReaders.cpp @@ -0,0 +1,233 @@ +// 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 +#include +#ifndef _WIN32 +#include +#include +#else +#include +#endif + +/// +struct TemporaryFile { + TemporaryFile(); + ~TemporaryFile(); + + TemporaryFile(const TemporaryFile&) = delete; + TemporaryFile& operator=(const TemporaryFile&) = delete; + + TemporaryFile(TemporaryFile&&) = default; + TemporaryFile& operator=(TemporaryFile&&) = default; + + const fs::path& path() const { return path_; } + +private: + 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: + void SetUp(const ::benchmark::State& state) override + { + workBuffer.resize(2 * static_cast(state.range(0))); + } + + void TearDown(const ::benchmark::State& /* state */) override + { + } + + static TemporaryFile createAudioFile(int format); + + static TemporaryFile fileWav; + static TemporaryFile fileFlac; + static TemporaryFile fileOgg; + + std::vector workBuffer; +}; + +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); + +TemporaryFile 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 temp file + TemporaryFile temp; + fprintf(stderr, "* Temporary file: %s\n", temp.path().u8string().c_str()); + + // 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 + + if (snd.error()) + throw std::runtime_error("cannot open sound file for writing"); + snd.writef(sndData.get(), fileFrames); + snd = SndfileHandle(); + + return temp; +} + +static void doReaderBenchmark(const fs::path& path, std::vector &buffer, sfz::AudioReaderType type) +{ + sfz::AudioReaderPtr reader = sfz::createExplicitAudioReader(path, type); + while (reader->readNextBlock(buffer.data(), buffer.size() / 2) > 0); +} + +static void doEntireRead(const fs::path& path) +{ +#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"); + + 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.path()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardWav)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileWav.path(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseWav)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileWav.path(), workBuffer, sfz::AudioReaderType::Reverse); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileFlac.path()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileFlac.path(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileFlac.path(), workBuffer, sfz::AudioReaderType::Reverse); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireOgg)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileOgg.path()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardOgg)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileOgg.path(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +//BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseOgg)(benchmark::State& state) +//{ +// for (auto _ : state) { +// doReaderBenchmark(fileOgg.path(), 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/BM_clamp.cpp b/benchmarks/BM_clamp.cpp new file mode 100644 index 000000000..beb66ae54 --- /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/BM_filterStereoMono.cpp b/benchmarks/BM_filterStereoMono.cpp index 16185812d..43030a244 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 9d17de3aa..4fedae8c3 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"; diff --git a/benchmarks/BM_meanSquared.cpp b/benchmarks/BM_meanSquared.cpp index 0b1c159ee..34c29a8fb 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/benchmarks/BM_multiplyMul.cpp b/benchmarks/BM_multiplyMul.cpp new file mode 100644 index 000000000..d002fba0b --- /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 000000000..56988bef9 --- /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/BM_pan_arm.cpp b/benchmarks/BM_pan_arm.cpp new file mode 100644 index 000000000..9ae03b2ff --- /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/BM_powerFollower.cpp b/benchmarks/BM_powerFollower.cpp new file mode 100644 index 000000000..6444e0e6c --- /dev/null +++ b/benchmarks/BM_powerFollower.cpp @@ -0,0 +1,125 @@ +// 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(); + + // + refFollower_.init(sfz::config::defaultSampleRate); + refFollower_.clear(); + } + + void TearDown(const ::benchmark::State& /* state */) + { + } + + 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_); + 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, ReferenceFollower)->Range(1, 1); +BENCHMARK_REGISTER_F(PowerFollowerFixture, Follower)->RangeMultiplier(2)->Range(1 << 5, 1 << 12); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 3c0082a1f..d53ff15fe 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) @@ -48,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) @@ -59,11 +62,15 @@ 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_allWithin BM_allWithin.cpp) 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}) @@ -78,6 +85,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) @@ -137,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/clients/CMakeLists.txt b/clients/CMakeLists.txt index 859005c08..80502f42a 100644 --- a/clients/CMakeLists.txt +++ b/clients/CMakeLists.txt @@ -3,10 +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 jack absl::flags_parse ${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 941d4120a..b9e5f5dde 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,19 +121,19 @@ 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; } -static bool shouldClose { false }; +static volatile sig_atomic_t shouldClose { false }; 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()) diff --git a/clients/sfizz_render.cpp b/clients/sfizz_render.cpp index 4d2a3fd4f..bd03ea201 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, quality); synth.enableFreeWheeling(); if (params.count("log") > 0) diff --git a/cmake/LV2Config.cmake b/cmake/LV2Config.cmake index 95c54bdfd..6b9d9b76e 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 16c805bdc..b488b4dbf 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -17,6 +17,31 @@ 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) +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 @@ -32,10 +57,15 @@ 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 + add_compile_options(-Wno-multichar) + add_compile_options(-Werror=return-type) if (SFIZZ_SYSTEM_PROCESSOR MATCHES "^(i.86|x86_64)$") add_compile_options(-msse2) + elseif(SFIZZ_SYSTEM_PROCESSOR MATCHES "^(arm.*)$") + add_compile_options(-mfpu=neon) + if (NOT ANDROID) + add_compile_options(-mfloat-abi=hard) + endif() endif() elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") set(CMAKE_CXX_STANDARD 17) @@ -43,22 +73,34 @@ 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() + +# 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(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) 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() @@ -107,12 +149,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/cmake/SfizzSIMDSourceFiles.cmake b/cmake/SfizzSIMDSourceFiles.cmake index 3cc9afdd0..83957c5dc 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/cmake/VSTConfig.cmake b/cmake/VSTConfig.cmake index dd2ae1cb6..6eea1a0ac 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[3-8][a-z]*)$") + set(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() diff --git a/common.mk b/common.mk new file mode 100644 index 000000000..99a83ee16 --- /dev/null +++ b/common.mk @@ -0,0 +1,296 @@ +# 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 + +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 + +### + +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 + +ifdef SFIZZ_OS_WINDOWS +SFIZZ_LINK_FLAGS += -ldbghelp +endif + +### 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 + +### 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/doxygen/layout/DoxygenLayout.xml b/doxygen/layout/DoxygenLayout.xml deleted file mode 100644 index 482372268..000000000 --- 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 6f98db557..000000000 --- a/doxygen/layout/custom_footer.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
-

Copyright © 2019-2020 Paul Ferrand - Generated for sfizz by - Doxygen $doxygenversion -

-
- - - - diff --git a/doxygen/layout/custom_header.html b/doxygen/layout/custom_header.html deleted file mode 100644 index bf5a07f63..000000000 --- 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 116008da8..000000000 --- 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 ab4905e91..000000000 --- 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 a7e12b3f1..000000000 --- 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 af618b6f2..000000000 --- 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/dpf.mk b/dpf.mk index 772dd364a..915db1663 100644 --- a/dpf.mk +++ b/dpf.mk @@ -1,3 +1,4 @@ + # # A build file to help using sfizz with the DISTRHO Plugin Framework (DPF) # ------------------------------------------------------------------------ @@ -36,14 +37,9 @@ 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 - -ifeq ($(LINUX),true) -SFIZZ_LINK_FLAGS += -pthread -endif +SFIZZ_PKG_CONFIG ?= $(PKG_CONFIG) +include $(SFIZZ_DIR)/common.mk sfizz-all: sfizz-lib @@ -54,198 +50,6 @@ sfizz-clean: .PHONY: sfizz-all sfizz-lib sfizz-clean -SFIZZ_SOURCES = \ - src/sfizz/ADSREnvelope.cpp \ - src/sfizz/Curve.cpp \ - src/sfizz/effects/Apan.cpp \ - src/sfizz/Effects.cpp \ - src/sfizz/effects/Eq.cpp \ - src/sfizz/effects/Filter.cpp \ - src/sfizz/effects/Gain.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/FileInstrument.cpp \ - src/sfizz/FilePool.cpp \ - src/sfizz/FilterPool.cpp \ - src/sfizz/FloatEnvelopes.cpp \ - src/sfizz/Logger.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/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/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/editor/CMakeLists.txt b/editor/CMakeLists.txt new file mode 100644 index 000000000..8f676d7e8 --- /dev/null +++ b/editor/CMakeLists.txt @@ -0,0 +1,88 @@ +set(VSTGUI_BASEDIR "${CMAKE_CURRENT_SOURCE_DIR}/external/vstgui4") +include("cmake/Vstgui.cmake") + +set(EDITOR_RESOURCES + logo.png + logo_text.png + logo_text_white.png + logo_text@2x.png + logo_text_white@2x.png + background.png + background@2x.png + icon_white.png + icon_white@2x.png + knob48.png + knob48@2x.png + Fonts/sfizz-fluentui-system-r20.ttf + Fonts/Roboto-Regular.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 + 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/GUIPiano.h + src/editor/GUIPiano.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_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 new file mode 100644 index 000000000..0ad30b360 --- /dev/null +++ b/editor/cmake/Vstgui.cmake @@ -0,0 +1,224 @@ +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/platform/platformfactory.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/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" + "${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/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" + "${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/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" + "${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 + target_link_libraries(sfizz-vstgui PRIVATE + "opengl32" + "d2d1" + "dwrite" + "dwmapi" + "windowscodecs" + "shlwapi") + endif() +elseif(APPLE) + 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) + pkg_check_modules(GLIB REQUIRED glib-2.0) + 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} + ${GLIB_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} + ${GLIB_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() + +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") + 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") +endif() diff --git a/editor/external/vstgui4 b/editor/external/vstgui4 new file mode 160000 index 000000000..055cbcc9a --- /dev/null +++ b/editor/external/vstgui4 @@ -0,0 +1 @@ +Subproject commit 055cbcc9ae858f0b07d5d86c205a1111e2fba7a4 diff --git a/editor/layout/main.fl b/editor/layout/main.fl new file mode 100644 index 000000000..59d22a06b --- /dev/null +++ b/editor/layout/main.fl @@ -0,0 +1,312 @@ +# data file for the Fltk User Interface Designer (fluid) +version 1.0305 +header_name {.h} +code_name {.cxx} +widget_class mainView {open + xywh {576 416 800 475} type Double + class LogicalGroup visible +} { + Fl_Box {} { + image {../resources/background.png} xywh {190 110 600 280} + class Background + } + Fl_Group {} { + comment {theme=darkTheme} open + xywh {0 0 800 110} + class LogicalGroup + } { + Fl_Group {} {open + xywh {5 4 175 101} box ROUNDED_BOX align 0 + class RoundedGroup + } { + Fl_Box {} { + comment {tag=kTagFirstChangePanel+kPanelGeneral} + image {../resources/logo_text_white.png} xywh {35 9 120 60} + class SfizzMainButton + } + Fl_Button {} { + comment {tag=kTagFirstChangePanel+kPanelGeneral} + xywh {49 73 25 25} labelsize 24 + class HomeButton + } + Fl_Button {} { + comment {tag=kTagFirstChangePanel+kPanelControls} + xywh {81 73 25 25} labelsize 24 + class CCButton + } + Fl_Button {} { + comment {tag=kTagFirstChangePanel+kPanelSettings} + xywh {112 73 25 25} labelsize 24 + class SettingsButton + } + } + Fl_Group {} {open + xywh {185 5 380 100} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Box {} { + label {Separator 1} + xywh {195 41 360 5} box BORDER_BOX labeltype NO_LABEL + class HLine + } + Fl_Box {} { + label {Separator 2} + xywh {195 73 360 5} box BORDER_BOX labeltype NO_LABEL + class HLine + } + Fl_Box sfzFileLabel_ { + label {DefaultInstrument.sfz} + comment {tag=kTagLoadSfzFile} + xywh {195 11 250 31} labelsize 20 align 20 + class ClickableLabel + } + Fl_Box {} { + label {Key switch:} + xywh {195 44 250 30} labelsize 20 align 20 + class Label + } + Fl_Box {} { + label {Voices:} + xywh {195 76 60 25} labelsize 12 align 24 + class Label + } + Fl_Button {} { + comment {tag=kTagPreviousSfzFile} + xywh {480 14 25 25} labelsize 24 + class PreviousFileButton + } + Fl_Button {} { + comment {tag=kTagNextSfzFile} + xywh {505 14 25 25} labelsize 24 + class NextFileButton + } + Fl_Button fileOperationsMenu_ { + comment {tag=kTagFileOperations} + xywh {530 14 25 25} labelsize 24 + class ChevronDropDown + } + Fl_Box infoVoicesLabel_ { + xywh {260 76 50 25} labelsize 12 align 16 + class Label + } + Fl_Box {} { + label {Max:} + xywh {315 76 60 25} labelsize 12 align 24 + class Label + } + Fl_Box numVoicesLabel_ { + xywh {380 76 50 25} labelsize 12 align 16 + class Label + } + Fl_Box {} { + label {Memory:} + xywh {435 76 60 25} labelsize 12 align 24 + class Label + } + Fl_Box memoryLabel_ { + xywh {500 76 50 25} labelsize 12 align 16 + class Label + } + } + Fl_Group {} {open + xywh {570 5 225 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 5} labelsize 12 hide + class ValueLabel + } + Fl_Dial volumeSlider_ { + comment {tag=kTagSetVolume} + xywh {680 20 48 48} value 0.5 + class StyledKnob + } + Fl_Box volumeLabel_ { + label {0.0 dB} + xywh {675 70 60 22} labelsize 12 + class ValueLabel + } + Fl_Box {} { + xywh {745 20 35 55} box BORDER_BOX + class VMeter + } + } + } + Fl_Group {subPanels_[kPanelGeneral]} { + xywh {5 110 791 285} + class LogicalGroup + } { + Fl_Group {} {open + xywh {5 110 175 280} box ROUNDED_BOX + class RoundedGroup + } { + Fl_Box {} { + label {Curves:} + xywh {20 120 60 25} align 20 + class Label + } + Fl_Box {} { + label {Masters:} + xywh {20 145 60 25} align 20 + class Label + } + Fl_Box {} { + label {Groups:} + xywh {20 170 60 25} align 20 + class Label + } + Fl_Box {} { + label {Regions:} + xywh {20 195 60 25} align 20 + class Label + } + Fl_Box {} { + label {Samples:} + xywh {20 220 60 25} align 20 + class Label + } + Fl_Box infoCurvesLabel_ { + label 0 + xywh {120 120 40 25} align 16 + class Label + } + Fl_Box infoMastersLabel_ { + label 0 + xywh {120 145 40 25} align 16 + class Label + } + Fl_Box infoGroupsLabel_ { + label 0 + xywh {120 170 40 25} align 16 + class Label + } + Fl_Box infoRegionsLabel_ { + label 0 + xywh {120 195 40 25} align 16 + class Label + } + Fl_Box infoSamplesLabel_ { + label 0 + xywh {120 220 40 25} align 16 + class Label + } + } + } + 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 109 790 286} hide + class LogicalGroup + } { + Fl_Group {} { + label Engine open + xywh {260 135 280 100} box ROUNDED_BOX labelsize 12 align 17 + class TitleGroup + } { + Fl_Spinner numVoicesSlider_ { + comment {tag=kTagSetNumVoices} + xywh {285 195 60 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Box {} { + label Polyphony + xywh {275 155 80 25} labelsize 12 + class ValueLabel + } + Fl_Spinner oversamplingSlider_ { + comment {tag=kTagSetOversampling} + xywh {370 195 60 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Box {} { + label Oversampling + xywh {360 155 80 25} labelsize 12 + class ValueLabel + } + Fl_Box {} { + label {Preload size} + xywh {445 155 80 25} labelsize 12 + class ValueLabel + } + Fl_Spinner preloadSizeSlider_ { + comment {tag=kTagSetPreloadSize} + xywh {455 195 60 25} labelsize 12 textsize 12 + class ValueMenu + } + } + Fl_Group {} { + label Tuning open + xywh {205 270 390 100} box ROUNDED_BOX labelsize 12 align 17 + class TitleGroup + } { + Fl_Box {} { + label {Root key} + xywh {330 290 80 25} labelsize 12 + class ValueLabel + } + Fl_Spinner tuningFrequencySlider_ { + comment {tag=kTagSetTuningFrequency} + xywh {425 330 60 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Box {} { + label Frequency + xywh {415 290 80 25} labelsize 12 + class ValueLabel + } + Fl_Dial stretchedTuningSlider_ { + comment {tag=kTagSetStretchedTuning} + xywh {515 315 48 48} value 0.5 + class StyledKnob + } + Fl_Box {} { + label Stretch + xywh {500 290 80 25} labelsize 12 + class ValueLabel + } + Fl_Box {} { + label {Scala file} + xywh {225 290 100 25} labelsize 12 + class ValueLabel + } + Fl_Button scalaFileButton_ { + label DefaultScale + comment {tag=kTagLoadScalaFile} + xywh {225 330 100 25} labelsize 12 + class ValueButton + } + Fl_Spinner scalaRootKeySlider_ { + comment {tag=kTagSetScalaRootKey} + xywh {340 330 35 25} labelsize 12 textsize 12 + class ValueMenu + } + Fl_Spinner scalaRootOctaveSlider_ { + comment {tag=kTagSetScalaRootKey} + xywh {375 330 30 25} labelsize 12 textsize 12 + class ValueMenu + } + } + } + Fl_Box piano_ {selected + xywh {5 400 790 70} labelsize 12 + class Piano + } +} diff --git a/editor/resources/Fonts/Roboto-Regular.ttf b/editor/resources/Fonts/Roboto-Regular.ttf new file mode 100644 index 000000000..2b6392ffe Binary files /dev/null and b/editor/resources/Fonts/Roboto-Regular.ttf differ diff --git a/editor/resources/Fonts/sfizz-fluentui-system-r20.ttf b/editor/resources/Fonts/sfizz-fluentui-system-r20.ttf new file mode 100644 index 000000000..dc27711e7 Binary files /dev/null and b/editor/resources/Fonts/sfizz-fluentui-system-r20.ttf differ diff --git a/editor/resources/background.png b/editor/resources/background.png new file mode 100644 index 000000000..671268679 Binary files /dev/null and b/editor/resources/background.png differ diff --git a/editor/resources/background@2x.png b/editor/resources/background@2x.png new file mode 100644 index 000000000..9177e38b0 Binary files /dev/null and b/editor/resources/background@2x.png differ diff --git a/editor/resources/icon_white.png b/editor/resources/icon_white.png new file mode 100644 index 000000000..d63b6f3df Binary files /dev/null and b/editor/resources/icon_white.png differ diff --git a/editor/resources/icon_white.svg b/editor/resources/icon_white.svg new file mode 100644 index 000000000..4021b7876 --- /dev/null +++ b/editor/resources/icon_white.svg @@ -0,0 +1,253 @@ + + + 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 000000000..e5bc80a22 Binary files /dev/null and b/editor/resources/icon_white@2x.png differ diff --git a/editor/resources/knob.knob b/editor/resources/knob.knob new file mode 100644 index 000000000..093bd66cc Binary files /dev/null and b/editor/resources/knob.knob differ diff --git a/editor/resources/knob48.png b/editor/resources/knob48.png new file mode 100644 index 000000000..d0fc774ab Binary files /dev/null and b/editor/resources/knob48.png differ diff --git a/editor/resources/knob48@2x.png b/editor/resources/knob48@2x.png new file mode 100644 index 000000000..f064cfc3e Binary files /dev/null and b/editor/resources/knob48@2x.png differ 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/resources/logo_text.png b/editor/resources/logo_text.png new file mode 100644 index 000000000..d683e44e1 Binary files /dev/null and b/editor/resources/logo_text.png differ diff --git a/editor/resources/logo_text.svg b/editor/resources/logo_text.svg new file mode 100644 index 000000000..a62481c87 --- /dev/null +++ b/editor/resources/logo_text.svg @@ -0,0 +1,252 @@ + + + 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 000000000..c3e28c637 Binary files /dev/null and b/editor/resources/logo_text@2x.png differ diff --git a/editor/resources/logo_text_white.png b/editor/resources/logo_text_white.png new file mode 100644 index 000000000..312719e5d Binary files /dev/null and b/editor/resources/logo_text_white.png differ diff --git a/editor/resources/logo_text_white@2x.png b/editor/resources/logo_text_white@2x.png new file mode 100644 index 000000000..d3464ae7d Binary files /dev/null and b/editor/resources/logo_text_white@2x.png differ diff --git a/editor/src/editor/EditIds.cpp b/editor/src/editor/EditIds.cpp new file mode 100644 index 000000000..1d148fb04 --- /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 000000000..660853dba --- /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/EditValue.h b/editor/src/editor/EditValue.h new file mode 100644 index 000000000..b8d911ddd --- /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 new file mode 100644 index 000000000..dc4870b24 --- /dev/null +++ b/editor/src/editor/Editor.cpp @@ -0,0 +1,1175 @@ +// 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 "GUIPiano.h" +#include "NativeHelpers.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utility/vstgui_before.h" +#include "vstgui/vstgui.h" +#include "utility/vstgui_after.h" + +using namespace VSTGUI; + +const int Editor::viewWidth { 800 }; +const int Editor::viewHeight { 475 }; + +struct Editor::Impl : EditorController::Receiver, IControlListener { + EditorController* ctrl_ = nullptr; + CFrame* frame_ = nullptr; + SharedPointer mainView_; + + std::string currentSfzFile_; + std::string currentScalaFile_; + + enum { + kPanelGeneral, + kPanelControls, + kPanelSettings, + kNumPanels, + }; + + unsigned activePanel_ = 0; + CViewContainer* subPanels_[kNumPanels] = {}; + + enum { + kTagLoadSfzFile, + kTagEditSfzFile, + kTagPreviousSfzFile, + kTagNextSfzFile, + kTagFileOperations, + kTagSetVolume, + kTagSetNumVoices, + kTagSetOversampling, + kTagSetPreloadSize, + kTagLoadScalaFile, + kTagSetScalaRootKey, + kTagSetTuningFrequency, + kTagSetStretchedTuning, + kTagFirstChangePanel, + kTagLastChangePanel = kTagFirstChangePanel + kNumPanels - 1, + }; + + STextButton* sfzFileLabel_ = nullptr; + CTextLabel* scalaFileLabel_ = nullptr; + STextButton* scalaFileButton_ = nullptr; + CControl *volumeSlider_ = nullptr; + CTextLabel* volumeLabel_ = nullptr; + SValueMenu *numVoicesSlider_ = nullptr; + CTextLabel* numVoicesLabel_ = nullptr; + SValueMenu *oversamplingSlider_ = nullptr; + CTextLabel* oversamplingLabel_ = nullptr; + SValueMenu *preloadSizeSlider_ = nullptr; + CTextLabel* preloadSizeLabel_ = nullptr; + SValueMenu *scalaRootKeySlider_ = nullptr; + SValueMenu *scalaRootOctaveSlider_ = nullptr; + CTextLabel* scalaRootKeyLabel_ = nullptr; + SValueMenu *tuningFrequencySlider_ = nullptr; + CTextLabel* tuningFrequencyLabel_ = nullptr; + CControl *stretchedTuningSlider_ = nullptr; + CTextLabel* stretchedTuningLabel_ = nullptr; + + CTextLabel* infoCurvesLabel_ = nullptr; + CTextLabel* infoMastersLabel_ = nullptr; + CTextLabel* infoGroupsLabel_ = nullptr; + CTextLabel* infoRegionsLabel_ = nullptr; + CTextLabel* infoSamplesLabel_ = nullptr; + CTextLabel* infoVoicesLabel_ = nullptr; + + CTextLabel* memoryLabel_ = nullptr; + + SActionMenu* fileOperationsMenu_ = nullptr; + + SPiano* piano_ = 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 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); + void updateScalaFileLabel(const std::string& filePath); + static void updateLabelWithFileName(CTextLabel* label, 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); + 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_; + + close(); + + EditorController& ctrl = *impl.ctrl_; + ctrl.decorate(nullptr); +} + +void Editor::open(CFrame& frame) +{ + Impl& impl = *impl_; + + impl.frame_ = &frame; + frame.addView(impl.mainView_.get()); +} + +void Editor::close() +{ + Impl& impl = *impl_; + + if (impl.frame_) { + impl.frame_->removeView(impl.mainView_.get(), false); + impl.frame_ = nullptr; + } +} + +void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) +{ + switch (id) { + 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_) + volumeSlider_->setValue(value); + updateVolumeLabel(value); + } + break; + case EditId::Polyphony: + { + const int value = static_cast(v.to_float()); + if (numVoicesSlider_) + numVoicesSlider_->setValue(value); + updateNumVoicesLabel(value); + } + break; + case EditId::Oversampling: + { + const int value = static_cast(v.to_float()); + + 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(v.to_float()); + if (preloadSizeSlider_) + preloadSizeSlider_->setValue(value); + updatePreloadSizeLabel(value); + } + break; + case EditId::ScalaFile: + { + const std::string& value = v.to_string(); + currentScalaFile_ = value; + updateScalaFileLabel(value); + } + break; + case EditId::ScalaRootKey: + { + const int value = std::max(0, static_cast(v.to_float())); + if (scalaRootKeySlider_) + scalaRootKeySlider_->setValue(value % 12); + if (scalaRootOctaveSlider_) + scalaRootOctaveSlider_->setValue(value / 12); + updateScalaRootKeyLabel(value); + } + break; + case EditId::TuningFrequency: + { + const float value = v.to_float(); + if (tuningFrequencySlider_) + tuningFrequencySlider_->setValue(value); + updateTuningFrequencyLabel(value); + } + break; + case EditId::StretchTuning: + { + const float value = v.to_float(); + if (stretchedTuningSlider_) + stretchedTuningSlider_->setValue(value); + updateStretchedTuningLabel(value); + } + break; + case EditId::UINumCurves: + { + 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(v.to_float()); + if (CTextLabel* label = infoMastersLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UINumGroups: + { + 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(v.to_float()); + if (CTextLabel* label = infoRegionsLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UINumPreloadedSamples: + { + 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(v.to_float()); + if (CTextLabel* label = infoVoicesLabel_) + formatLabel(label, "%u", value); + } + break; + case EditId::UIActivePanel: + { + const int value = static_cast(v.to_float()); + setActivePanel(value); + } + break; + } +} + +void Editor::Impl::createFrameContents() +{ + CViewContainer* mainView; + + 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")); + + { + const CColor frameBackground = { 0xd3, 0xd7, 0xcf }; + + struct Theme { + CColor boxBackground; + CColor text; + CColor inactiveText; + CColor highlightedText; + CColor titleBoxText; + CColor titleBoxBackground; + CColor icon; + CColor iconHighlight; + CColor valueText; + CColor valueBackground; + CColor knobActiveTrackColor; + CColor knobInactiveTrackColor; + CColor knobLineIndicatorColor; + }; + + 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; + lightTheme.iconHighlight = { 0xfd, 0x98, 0x00 }; + 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 }; + darkTheme.inactiveText = { 0xb2, 0xb2, 0xb2 }; + darkTheme.highlightedText = { 0xfd, 0x98, 0x00 }; + darkTheme.titleBoxText = { 0x00, 0x00, 0x00 }; + darkTheme.titleBoxBackground = { 0xba, 0xbd, 0xb6 }; + darkTheme.icon = darkTheme.text; + darkTheme.iconHighlight = { 0xfd, 0x98, 0x00 }; + 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; + 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 CAnimKnob Knob48; + typedef SStyledKnob StyledKnob; + typedef CTextLabel ValueLabel; + typedef CViewContainer VMeter; + typedef SValueMenu ValueMenu; + typedef CViewContainer Background; +#if 0 + typedef CTextButton Button; +#endif + typedef STextButton ClickableLabel; + typedef STextButton ValueButton; + typedef STextButton LoadFileButton; + typedef STextButton CCButton; + typedef STextButton HomeButton; + typedef STextButton SettingsButton; + typedef STextButton EditFileButton; + 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); + 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("Roboto", 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("Roboto", 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 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)); + lbl->setBackColor(CColor(0x00, 0x00, 0x00, 0x00)); + lbl->setFontColor(theme->text); + lbl->setHoriAlign(align); + auto font = owned(new CFontDesc("Roboto", 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; + }; +#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("Roboto", fontsize)); + button->setFont(font); + button->setTextAlignment(align); + 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) { + 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); + 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("Roboto", 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) { + STextButton* btn = new STextButton(bounds, this, tag, glyph); + btn->setFont(new CFontDesc("Sfizz Fluent System R20", fontsize)); + 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; + }; + 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"\ue253", 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); + }; + 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 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) { + 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); + return container; + }; + + #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, "%lu kB", static_cast(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 * sizeof(float))))) + " 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; + }); + + if (SActionMenu* menu = fileOperationsMenu_) { + menu->addEntry("Load file", kTagLoadSfzFile); + 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; + + // all panels + for (unsigned currentPanel = 0; currentPanel < kNumPanels; ++currentPanel) { + panel = subPanels_[currentPanel]; + + if (!panel) + continue; + + panel->setVisible(currentPanel == activePanel_); + } +} + +void Editor::Impl::chooseSfzFile() +{ + SharedPointer fs = owned(CNewFileSelector::create(frame_)); + + 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); + if (file) + changeSfzFile(file); + } +} + +void Editor::Impl::changeSfzFile(const std::string& filePath) +{ + ctrl_->uiSendValue(EditId::SfzFile, filePath); + currentSfzFile_ = 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_)); + + 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); + if (file) + changeScalaFile(file); + } +} + +void Editor::Impl::changeScalaFile(const std::string& filePath) +{ + ctrl_->uiSendValue(EditId::ScalaFile, filePath); + currentScalaFile_ = 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()) + 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) +{ + updateButtonWithFileName(sfzFileLabel_, filePath, ".sfz"); +} + +void Editor::Impl::updateScalaFileLabel(const std::string& filePath) +{ + updateLabelWithFileName(scalaFileLabel_, filePath, ".scl"); + updateButtonWithFileName(scalaFileButton_, filePath, ".scl"); +} + +void Editor::Impl::updateLabelWithFileName(CTextLabel* label, const std::string& filePath, absl::string_view removedSuffix) +{ + if (!label) + return; + + std::string fileName = std::string(simplifiedFileName(filePath, removedSuffix, "")); + label->setText(fileName.c_str()); +} + +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, {})); + 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) +{ + 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, "%d kB", static_cast(std::round(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) { + if (subPanels_[activePanel_]) + subPanels_[activePanel_]->setVisible(false); + if (subPanels_[panelId]) + subPanels_[panelId]->setVisible(true); + activePanel_ = panelId; + } +} + +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 kTagEditSfzFile: + if (value != 1) + break; + + if (!currentSfzFile_.empty()) + 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; + + 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, static_cast(1 << static_cast(value))); + updateOversamplingLabel(static_cast(value)); + break; + + case kTagSetPreloadSize: + ctrl.uiSendValue(EditId::PreloadSize, value); + updatePreloadSizeLabel(static_cast(value)); + break; + + case kTagSetScalaRootKey: + { + 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: + 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, static_cast(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 000000000..0c0ae1de8 --- /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 000000000..4d3db4c99 --- /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 "EditValue.h" +#include +#include +#include +enum class EditId : int; + +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/editor/src/editor/GUIComponents.cpp b/editor/src/editor/GUIComponents.cpp new file mode 100644 index 000000000..803698ddb --- /dev/null +++ b/editor/src/editor/GUIComponents.cpp @@ -0,0 +1,495 @@ +// 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 "GUIComponents.h" +#include +#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" + +/// +SBoxContainer::SBoxContainer(const CRect& size) + : CViewContainer(size) +{ + CViewContainer::setBackgroundColor(CColor(0, 0, 0, 0)); +} + +void SBoxContainer::setCornerRadius(CCoord radius) +{ + cornerRadius_ = radius; + invalid(); +} + +void SBoxContainer::setBackgroundColor(const CColor& color) +{ + backgroundColor_ = color; + invalid(); +} + +CColor SBoxContainer::getBackgroundColor() const +{ + return backgroundColor_; +} + +void SBoxContainer::drawRect(CDrawContext* dc, const CRect& updateRect) +{ + CRect bounds = getViewSize(); + + dc->setDrawMode(kAntiAliasing); + + SharedPointer path = owned(dc->createGraphicsPath()); + path->addRoundRect(bounds, cornerRadius_); + + 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; + invalid(); +} + +void STitleContainer::setTitleFontColor(CColor color) +{ + titleFontColor_ = color; + invalid(); +} + +void STitleContainer::setTitleBackgroundColor(CColor color) +{ + titleBackgroundColor_ = color; + invalid(); +} + +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; +} + +/// +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->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 SValueMenu::onItemClicked(int32_t index) +{ + float oldValue = getValue(); + setValue(menuItemValues_[index]); + if (getValue() != oldValue) + 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; + invalid(); +} + +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) +{ + 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_) + textColor = hoverColor_; // textColor is protected + else if (inactive_) + textColor = inactiveColor_; + CTextButton::draw(context); + textColor = backupColor; +} + + +CMouseEventResult STextButton::onMouseEntered (CPoint& where, const CButtonState& buttons) +{ + hovered_ = true; + invalid(); + return CTextButton::onMouseEntered(where, buttons); +} + +CMouseEventResult STextButton::onMouseExited (CPoint& where, const CButtonState& buttons) +{ + hovered_ = false; + invalid(); + 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; + invalid(); +} + +void SStyledKnob::setInactiveTrackColor(const CColor& color) +{ + if (inactiveTrackColor_ == color) + return; + inactiveTrackColor_ = color; + invalid(); +} + +void SStyledKnob::setLineIndicatorColor(const CColor& color) +{ + if (lineIndicatorColor_ == color) + return; + lineIndicatorColor_ = color; + invalid(); +} + +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 new file mode 100644 index 000000000..5a807b98e --- /dev/null +++ b/editor/src/editor/GUIComponents.h @@ -0,0 +1,210 @@ +// 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 "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/cbuttons.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 SBoxContainer : public CViewContainer { +public: + explicit SBoxContainer(const CRect& size); + virtual ~SBoxContainer() {} + void setCornerRadius(CCoord radius); + void setBackgroundColor(const CColor& color) override; + CColor getBackgroundColor() const override; + +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: + 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 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) override; + +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_; + }; +}; + +/// +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); + 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 }; + CColor inactiveColor_; + bool inactive_ { 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/GUIPiano.cpp b/editor/src/editor/GUIPiano.cpp new file mode 100644 index 000000000..dd7d6cf58 --- /dev/null +++ b/editor/src/editor/GUIPiano.cpp @@ -0,0 +1,238 @@ +// 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 +#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(-2 * innerPaddingX_, -2 * 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( + 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 + spacingY_, + 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 000000000..6927b06f5 --- /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/NativeHelpers.cpp b/editor/src/editor/NativeHelpers.cpp new file mode 100644 index 000000000..ab760d73c --- /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 000000000..b9c29bb67 --- /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 000000000..9cb8cd28d --- /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 000000000..0c9c61e21 --- /dev/null +++ b/editor/src/editor/layout/main.hpp @@ -0,0 +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; +Background* const view__1 = createBackground(CRect(190, 110, 790, 390), -1, "", kCenterText, 14); +view__0->addView(view__1); +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); +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); +HLine* const view__9 = createHLine(CRect(10, 36, 370, 41), -1, "", kCenterText, 14); +view__8->addView(view__9); +HLine* const view__10 = createHLine(CRect(10, 68, 370, 73), -1, "", kCenterText, 14); +view__8->addView(view__10); +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); +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(295, 9, 320, 34), kTagPreviousSfzFile, "", kCenterText, 24); +view__8->addView(view__14); +NextFileButton* const view__15 = createNextFileButton(CRect(320, 9, 345, 34), kTagNextSfzFile, "", kCenterText, 24); +view__8->addView(view__15); +ChevronDropDown* const view__16 = createChevronDropDown(CRect(345, 9, 370, 34), kTagFileOperations, "", kCenterText, 24); +fileOperationsMenu_ = view__16; +view__8->addView(view__16); +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(130, 71, 190, 96), -1, "Max:", kRightText, 12); +view__8->addView(view__18); +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(250, 71, 310, 96), -1, "Memory:", kRightText, 12); +view__8->addView(view__20); +Label* const view__21 = createLabel(CRect(315, 71, 365, 96), -1, "", kCenterText, 12); +memoryLabel_ = view__21; +view__8->addView(view__21); +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); +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__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(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, 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); +numVoicesSlider_ = view__45; +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, 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, 12); +piano_ = view__61; +view__0->addView(view__61); diff --git a/editor/src/editor/utility/vstgui_after.h b/editor/src/editor/utility/vstgui_after.h new file mode 100644 index 000000000..69e3b4db9 --- /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 000000000..8293e82b7 --- /dev/null +++ b/editor/src/editor/utility/vstgui_before.h @@ -0,0 +1,14 @@ +// 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" +#pragma GCC diagnostic ignored "-Wmultichar" +#pragma GCC diagnostic ignored "-Wextra" +#endif diff --git a/editor/tools/layout-maker/LICENSE b/editor/tools/layout-maker/LICENSE new file mode 100644 index 000000000..36b7cd93c --- /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 000000000..e8402cc3e --- /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 000000000..35a1978e4 --- /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 000000000..06e5283b0 --- /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 000000000..5890951b5 --- /dev/null +++ b/editor/tools/layout-maker/sources/reader.cpp @@ -0,0 +1,298 @@ +#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_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 000000000..589bd307c --- /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/external/jsl/LICENSE.md b/external/jsl/LICENSE.md new file mode 100644 index 000000000..44da875b0 --- /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 000000000..e97c25a33 --- /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 000000000..d42e65850 --- /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 000000000..cfb54b03a --- /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 000000000..960ea5ad6 --- /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/lv2/CMakeLists.txt b/lv2/CMakeLists.txt index 73de6ee56..a888ca816 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,33 +23,63 @@ 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/$<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/$<0:>") +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_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() @@ -54,12 +88,21 @@ 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") + copy_editor_resources( + "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources" + "${PROJECT_BINARY_DIR}/Contents/Resources") +endif() + # Installation if (NOT MSVC) install (DIRECTORY ${PROJECT_BINARY_DIR} DESTINATION ${LV2PLUGIN_INSTALL_DIR} diff --git a/lv2/lv2/atom/util.h b/lv2/lv2/atom/util.h index 051a3cb2b..9c372aab3 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)) diff --git a/lv2/lv2ui.version b/lv2/lv2ui.version new file mode 100644 index 000000000..95c55e1b7 --- /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 3ea4e8838..ab407d332 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 cdb18992c..b68c527f2 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 "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" -// 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 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)) @@ -114,6 +105,12 @@ typedef struct const float *scala_root_key_port; 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 @@ -129,8 +126,11 @@ 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; + LV2_URID atom_long_uri; LV2_URID atom_urid_uri; LV2_URID atom_path_uri; LV2_URID patch_set_uri; @@ -147,7 +147,14 @@ 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; + 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; @@ -164,24 +171,24 @@ 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; 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_TIMEINFO_POSITION = 1 << 0, + SFIZZ_TIMEINFO_SIGNATURE = 1 << 1, + SFIZZ_TIMEINFO_TEMPO = 1 << 2, + SFIZZ_TIMEINFO_SPEED = 1 << 3, }; static void @@ -207,10 +214,13 @@ 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); + 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); @@ -224,8 +234,71 @@ 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->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 @@ -272,6 +345,24 @@ 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; + 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; } @@ -319,6 +410,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, @@ -354,6 +458,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++) { @@ -465,6 +577,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; } @@ -509,7 +623,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_handle_atom_object(sfizz_plugin_t *self, const LV2_Atom_Object *obj) { @@ -745,8 +858,10 @@ 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) + 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) @@ -773,7 +888,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 { @@ -802,6 +970,12 @@ 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); + *(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 6c6619876..5ddd03173 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: . @@ -35,6 +36,12 @@ midnam:update a lv2:Feature . "Accordage"@fr , "Accordatura"@it . +<@LV2PLUGIN_URI@#status> + a pg:Group ; + lv2:symbol "status" ; + lv2:name "status", + "Statut"@fr . + <@LV2PLUGIN_URI@:sfzfile> a lv2:Parameter ; rdfs:label "SFZ file", @@ -77,8 +84,10 @@ 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> ; + @LV2PLUGIN_IF_ENABLE_UI@ui:ui <@LV2PLUGIN_URI@#ui> ; + + patch:writable <@LV2PLUGIN_URI@:sfzfile> , + <@LV2PLUGIN_URI@:tuningfile> ; lv2:port [ a lv2:InputPort, atom:AtomPort ; @@ -87,7 +96,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 ; @@ -95,7 +105,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 ; @@ -291,4 +302,70 @@ 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 ; + ] , [ + 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 régions"@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 000000000..b0eafe00a --- /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 000000000..70ff76e15 --- /dev/null +++ b/lv2/sfizz_ui.cpp @@ -0,0 +1,497 @@ +/* + 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" +#if defined(_WIN32) +#include "vstgui/lib/platform/platform_win32.h" +#endif +#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 { +#if LINUX + SoHandleInitializer soHandleInitializer; +#endif +#if MAC + BundleRefInitializer bundleRefInitializer; +#endif + 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; + FrameHolder uiFrame; + std::unique_ptr editor; +#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 defined(_WIN32) + IWin32PlatformFrame::setResourceBasePath((std::string(bundle_path) + "\\Contents\\Resources\\").c_str()); +#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 = owned(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(); +#else + (void)self; +#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) +{ + 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, v.to_float()); + break; + case EditId::Polyphony: + sendFloat(SFIZZ_POLYPHONY, v.to_float()); + break; + case EditId::Oversampling: + sendFloat(SFIZZ_OVERSAMPLING, v.to_float()); + break; + case EditId::PreloadSize: + sendFloat(SFIZZ_PRELOAD, v.to_float()); + break; + case EditId::ScalaRootKey: + sendFloat(SFIZZ_SCALA_ROOT_KEY, v.to_float()); + break; + case EditId::TuningFrequency: + sendFloat(SFIZZ_TUNING_FREQUENCY, v.to_float()); + break; + case EditId::StretchTuning: + sendFloat(SFIZZ_STRETCH_TUNING, v.to_float()); + break; + case EditId::SfzFile: + sendPath(sfizz_sfz_file_uri, v.to_string()); + break; + case EditId::ScalaFile: + sendPath(sfizz_scala_file_uri, v.to_string()); + 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 000000000..b5fb3470b --- /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 000000000..31637bb00 --- /dev/null +++ b/lv2/vstgui_helpers.cpp @@ -0,0 +1,216 @@ +// 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 +#include +#if LINUX +#include +#include +#endif +#if WINDOWS +#include +#endif +#if MAC +#include "vstgui/plugin-bindings/getpluginbundle.h" +#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 size_t soHandleCount = 0; +static std::mutex soHandleMutex; + +SoHandleInitializer::SoHandleInitializer() +{ + 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 + +/// +#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 + +/// +#if MAC +namespace VSTGUI +{ +void* gBundleRef = nullptr; + +static volatile size_t gBundleRefCount = 0; +static std::mutex gBundleRefMutex; + +BundleRefInitializer::BundleRefInitializer() +{ + std::lock_guard lock(gBundleRefMutex); + if (gBundleRefCount++ == 0) { + gBundleRef = GetPluginBundle(); + if (!gBundleRef) + throw std::runtime_error("BundleRefInitializer"); + } +} + +BundleRefInitializer::~BundleRefInitializer() +{ + std::lock_guard lock(gBundleRefMutex); + if (--gBundleRefCount == 0) { + CFRelease((CFBundleRef)gBundleRef); + gBundleRef = nullptr; + } +} + +} // namespace VSTGUI +#endif diff --git a/lv2/vstgui_helpers.h b/lv2/vstgui_helpers.h new file mode 100644 index 000000000..2fbcd841c --- /dev/null +++ b/lv2/vstgui_helpers.h @@ -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 + +// 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 +{ +class SoHandleInitializer { +public: + SoHandleInitializer(); + ~SoHandleInitializer(); +private: + SoHandleInitializer(const SoHandleInitializer&) = delete; + SoHandleInitializer& operator=(const SoHandleInitializer&) = delete; +}; +} +#endif + +#if MAC +namespace VSTGUI +{ +class BundleRefInitializer { +public: + BundleRefInitializer(); + ~BundleRefInitializer(); +private: + BundleRefInitializer(const BundleRefInitializer&) = delete; + BundleRefInitializer& operator=(const BundleRefInitializer&) = delete; +}; +} +#endif diff --git a/rack.mk b/rack.mk new file mode 100644 index 000000000..4f8d25127 --- /dev/null +++ b/rack.mk @@ -0,0 +1,88 @@ + +# +# 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 + +### + +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) diff --git a/doxygen/scripts/Doxyfile.in b/scripts/doxygen/Doxyfile.in similarity index 99% rename from doxygen/scripts/Doxyfile.in rename to scripts/doxygen/Doxyfile.in index 0f7eb945d..0465a14ea 100644 --- a/doxygen/scripts/Doxyfile.in +++ b/scripts/doxygen/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/scripts/doxygen/doxy2json.py b/scripts/doxygen/doxy2json.py new file mode 100644 index 000000000..4ca1aa695 --- /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/scripts/generate_compressor.sh b/scripts/generate_compressor.sh new file mode 100755 index 000000000..7d799a5e1 --- /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/scripts/generate_disto.sh b/scripts/generate_disto.sh new file mode 100755 index 000000000..095cb147c --- /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/scripts/generate_fverb.sh b/scripts/generate_fverb.sh new file mode 100755 index 000000000..7c997e2b5 --- /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/scripts/generate_gate.sh b/scripts/generate_gate.sh new file mode 100755 index 000000000..6cb324229 --- /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/scripts/generate_ui_fonts.sh b/scripts/generate_ui_fonts.sh new file mode 100755 index 000000000..8b0f48f27 --- /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" diff --git a/scripts/innosetup.iss.in b/scripts/innosetup.iss.in index d97d3f2b4..4ac737bda 100644 --- a/scripts/innosetup.iss.in +++ b/scripts/innosetup.iss.in @@ -48,16 +48,22 @@ 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\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"; 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" 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"; 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 +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 diff --git a/scripts/run_clang_tidy.sh b/scripts/run_clang_tidy.sh index a25f69f93..08ceb36cd 100755 --- a/scripts/run_clang_tidy.sh +++ b/scripts/run_clang_tidy.sh @@ -30,7 +30,8 @@ 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 \ -DNDEBUG -std=c++17 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8de6d8eb2..b4a556530 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,12 +7,19 @@ 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 + sfizz/effects/dsp/gate.dsp + sfizz/effects/dsp/disto_stage.dsp + sfizz/effects/dsp/fverb.dsp) 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 @@ -20,6 +27,18 @@ set (SFIZZ_HEADERS sfizz/Config.h sfizz/Curve.h 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 + 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 sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArraySSE.h @@ -29,9 +48,13 @@ set (SFIZZ_HEADERS sfizz/effects/Apan.h 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/Fverb.h sfizz/effects/Gain.h + sfizz/effects/Gate.h sfizz/effects/Limiter.h sfizz/effects/Lofi.h sfizz/effects/Nothing.h @@ -43,23 +66,26 @@ 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 + sfizz/FlexEGDescription.h + sfizz/FlexEnvelope.h sfizz/HistoricalBuffer.h sfizz/Interpolators.h sfizz/Interpolators.hpp sfizz/Logger.h + sfizz/LFO.h + sfizz/LFODescription.h sfizz/MathHelpers.h sfizz/MidiState.h sfizz/ModifierHelpers.h - sfizz/Modifiers.h - sfizz/NumericId.h sfizz/OnePoleFilter.h 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 @@ -92,7 +118,8 @@ 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 sfizz/Region.cpp @@ -114,13 +141,31 @@ set (SFIZZ_SOURCES sfizz/RTSemaphore.cpp sfizz/Panning.cpp sfizz/Effects.cpp + 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/ADSREnvelope.cpp + sfizz/modulations/sources/LFO.cpp + sfizz/utility/SpinMutex.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp sfizz/effects/Apan.cpp sfizz/effects/Lofi.cpp sfizz/effects/Limiter.cpp + sfizz/effects/Compressor.cpp + 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 @@ -173,7 +218,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) @@ -181,25 +226,14 @@ 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 - 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) 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) @@ -211,7 +245,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() @@ -219,14 +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() diff --git a/src/external/threadpool/ThreadPool.h b/src/external/threadpool/ThreadPool.h new file mode 100644 index 000000000..2e030687c --- /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; + volatile 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.h b/src/sfizz.h index 05034e7d6..ec47ad048 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.5.0 */ 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. + * + * 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 * - * @param synth The sfizz synth. - * @param path A null-terminated string representing a path to an SFZ - * file. + * @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. + * @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,201 +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. + * + * 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. + * @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. + * + * 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. + * @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. + * + * 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. + * @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. + * + * 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. + * @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. (CURRENTLY UNIMPLEMENTED) + * @brief Send a tempo event. + * @since 0.2.0 * - * @param synth The synth. - * @param delay The delay. - * @param seconds_per_quarter The seconds per quarter. + * @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_quarter); +SFIZZ_EXPORTED_API void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float seconds_per_beat); /** - * @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 Send the time signature. + * @since 0.5.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. + * @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. + * @since 0.5.0 + * + * @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. + * @since 0.5.0 + * + * @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. + * @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. @@ -348,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. - * - * 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. + * @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 sfizz_set_sample_rate() function. + * + * 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. + * + * 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. + * @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); @@ -548,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); @@ -564,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); @@ -572,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); @@ -580,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); @@ -587,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 40d9f424f..81ba6cae5 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 @@ class SFIZZ_EXPORTED_API Sfizz * 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 @@ class SFIZZ_EXPORTED_API Sfizz * @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 @@ class SFIZZ_EXPORTED_API Sfizz * @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 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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 @@ class SFIZZ_EXPORTED_API Sfizz 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 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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 @@ class SFIZZ_EXPORTED_API Sfizz 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 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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 @@ class SFIZZ_EXPORTED_API Sfizz * @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,36 +296,69 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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. (CURRENTLY UNIMPLEMENTED) + * @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 secondsPerBeat the new period of the beat. + */ + void tempo(int delay, float secondsPerBeat) noexcept; + + /** + * @brief Send the time signature. + * @since 0.5.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 secondsPerQuarter the new period of the quarter note. + * @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 tempo(int delay, float secondsPerQuarter) noexcept; + void timeSignature(int delay, int beatsPerBar, int beatUnit); /** - * @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 Send the time position. + * @since 0.5.0 + * + * @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. + * @since 0.5.0 + * + * @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]. + * @since 0.2.0 * * @param buffers the buffers to write the next block into. * @param numFrames the number of stereo frames in the block. @@ -312,20 +368,24 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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. */ @@ -333,6 +393,7 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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. * @@ -350,6 +411,7 @@ class SFIZZ_EXPORTED_API Sfizz * 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. * @@ -359,15 +421,18 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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. */ @@ -375,30 +440,37 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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; @@ -406,6 +478,7 @@ class SFIZZ_EXPORTED_API Sfizz * @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. @@ -416,23 +489,27 @@ class SFIZZ_EXPORTED_API Sfizz * @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. */ @@ -440,52 +517,54 @@ class SFIZZ_EXPORTED_API Sfizz /** * @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; }; diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index bb86115bc..803adbb67 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -11,22 +11,35 @@ namespace sfz { -template -void ADSREnvelope::reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept +template +Type ADSREnvelope::secondsToSamples (Type timeInSeconds) const noexcept { - auto secondsToSamples = [sampleRate](Type timeInSeconds) { - return static_cast(timeInSeconds * sampleRate); - }; + return static_cast(timeInSeconds * sampleRate); +}; - auto secondsToLinRate = [sampleRate](Type timeInSeconds) { - timeInSeconds = std::max(timeInSeconds, config::virtuallyZero); - return 1 / (sampleRate * timeInSeconds); - }; +template +Type ADSREnvelope::secondsToLinRate (Type timeInSeconds) const noexcept +{ + if (timeInSeconds == 0) + return 1.0f; - auto secondsToExpRate = [sampleRate](Type timeInSeconds) { - timeInSeconds = std::max(25e-3, timeInSeconds); - return std::exp(-8.0 / (timeInSeconds * sampleRate)); - }; + 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)); +}; + +template +void ADSREnvelope::reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept +{ + this->sampleRate = sampleRate; this->delay = delay + secondsToSamples(desc.getDelay(state, velocity)); this->attackStep = secondsToLinRate(desc.getAttack(state, velocity)); @@ -35,14 +48,15 @@ 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) - || (region.trigger == SfzTrigger::release_key) - || (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator))); + freeRunning = ( + (this->sustain == 0.0f) + || (region.loopMode == SfzLoopMode::one_shot && region.isOscillator()) + ); currentValue = this->start; currentState = State::Delay; } @@ -76,7 +90,7 @@ Type ADSREnvelope::getNextValue() noexcept // fallthrough case State::Decay: currentValue *= decayRate; - if (currentValue > sustain) + if (currentValue > sustainThreshold ) return currentValue; currentState = State::Sustain; @@ -88,7 +102,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; @@ -146,7 +160,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; } @@ -161,9 +175,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; } @@ -208,13 +222,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 608567c65..eafd2f90a 100644 --- a/src/sfizz/ADSREnvelope.h +++ b/src/sfizz/ADSREnvelope.h @@ -44,15 +44,18 @@ class ADSREnvelope { * @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 @@ class ADSREnvelope { 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, @@ -94,6 +102,7 @@ class ADSREnvelope { 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/src/sfizz/AudioReader.cpp b/src/sfizz/AudioReader.cpp new file mode 100644 index 000000000..e906e3ae7 --- /dev/null +++ b/src/sfizz/AudioReader.cpp @@ -0,0 +1,375 @@ +// 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, 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; +} + +static AudioReaderPtr createAudioReaderWithHandle(SndfileHandle handle, bool reverse, std::error_code* ec) +{ + AudioReaderPtr reader; + + if (ec) + ec->clear(); + + 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 createAudioReader(const fs::path& path, bool reverse, std::error_code* ec) +{ +#if defined(_WIN32) + SndfileHandle handle(path.wstring().c_str()); +#else + SndfileHandle handle(path.c_str()); +#endif + 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) + *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; +} + +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); +} + +} // namespace sfz diff --git a/src/sfizz/AudioReader.h b/src/sfizz/AudioReader.h new file mode 100644 index 000000000..64c661f1a --- /dev/null +++ b/src/sfizz/AudioReader.h @@ -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 + +#pragma once +#include "absl/types/span.h" +#include "ghc/fs_std.hpp" +#include +#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/AudioSpan.h b/src/sfizz/AudioSpan.h index 0791eafad..fa879312a 100644 --- a/src/sfizz/AudioSpan.h +++ b/src/sfizz/AudioSpan.h @@ -200,7 +200,7 @@ class AudioSpan { * @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 @@ class AudioSpan { * @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 @@ class AudioSpan { * * @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 @@ class AudioSpan { * * @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 @@ class AudioSpan { * * @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 @@ class AudioSpan { * * @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/BufferPool.h b/src/sfizz/BufferPool.h index 2d057edf4..b9971fcfc 100644 --- a/src/sfizz/BufferPool.h +++ b/src/sfizz/BufferPool.h @@ -36,6 +36,7 @@ class SpanHolder { this->value = other.value; this->available = other.available; other.available = nullptr; + return *this; } SpanHolder(T&& value, int* available) : value(std::forward(value)) diff --git a/src/sfizz/CCMap.h b/src/sfizz/CCMap.h index dce9813fe..4fcc13f85 100644 --- a/src/sfizz/CCMap.h +++ b/src/sfizz/CCMap.h @@ -38,6 +38,8 @@ class CCMap { 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 @@ class CCMap { // 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/Config.h b/src/sfizz/Config.h index db286b018..349a9c12a 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -31,13 +31,15 @@ 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 bool loadInRam { false }; constexpr int loggerQueueSize { 256 }; constexpr int voiceLoggerQueueSize { 256 }; 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 }; @@ -50,7 +52,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 }; @@ -58,33 +59,42 @@ namespace config { constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 }; constexpr float A440 { 440.0 }; constexpr size_t powerHistoryLength { 16 }; - constexpr float filteredEnvelopeCutoff { 5 }; + constexpr size_t powerFollowerStep { 512 }; + constexpr float powerFollowerAttackTime { 5e-3f }; + constexpr float powerFollowerReleaseTime { 200e-3f }; constexpr uint16_t numCCs { 512 }; constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; 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. */ 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 }; - 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 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 */ @@ -96,14 +106,39 @@ namespace config { constexpr int maxEffectBuses { 256 }; // Wavetable constants; amplitude values are matched to reference static constexpr unsigned tableSize = 1024; - static constexpr double amplitudeSine = 0.625; - static constexpr double amplitudeTriangle = 0.625; - static constexpr double amplitudeSaw = 0.515; - static constexpr double amplitudeSquare = 0.515; + static constexpr double tableRefSampleRate = 44100.0 * 1.1; // +10% aliasing permissivity + /** + 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)? + /** + 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 */ static constexpr int backgroundLoaderPthreadPriority = 50; // expressed in % + /** + @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 + /** + * @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, "This needs to add voices"); } // namespace config } // namespace sfz diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index 89320db58..8d204a398 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 3cf4b2cb0..5c0ed3610 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 @@ class Curve { */ 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/Debug.h b/src/sfizz/Debug.h index 2c2f270c4..97c8bea8c 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 diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 940b6872e..290c12481 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 }; @@ -54,19 +54,28 @@ 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 }; + constexpr Range float7Range { 0.0f, 127.0f }; constexpr Range normalizedRange { 0.0f, 1.0f }; constexpr Range symmetricNormalizedRange { -1.0, 1.0 }; // 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 }; constexpr Range oscillatorMultiRange { 1, config::oscillatorsPerVoice }; constexpr float oscillatorDetune { 0 }; - constexpr Range oscillatorDetuneRange { -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 }; constexpr int oscillatorQuality { 1 }; constexpr Range oscillatorQualityRange { 0, 3 }; @@ -74,6 +83,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 }; @@ -114,7 +124,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 }; @@ -142,6 +152,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 @@ -151,18 +162,18 @@ 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 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 { -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 }; @@ -190,32 +201,50 @@ 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, 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 }; 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, 1.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 }; 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 }; 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 }; @@ -223,9 +252,25 @@ 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 }; + 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 { 2 }; + constexpr int sampleQuality { 1 }; constexpr int sampleQualityInFreewheelingMode { 10 }; // for future use, possibly excessive constexpr Range sampleQualityRange { 1, 10 }; // sample_quality @@ -236,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/EGDescription.h b/src/sfizz/EGDescription.h index 683b4d519..5cfe0d943 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 }; diff --git a/src/sfizz/EQDescription.h b/src/sfizz/EQDescription.h index 161207df0..c109a4a08 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/EQPool.cpp b/src/sfizz/EQPool.cpp index 6d3d75b2d..bca8c711c 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -4,147 +4,85 @@ #include "SIMDHelpers.h" #include "SwapAndPop.h" -sfz::EQHolder::EQHolder(const MidiState& state) -:midiState(state) +sfz::EQHolder::EQHolder(Resources& resources) +: resources(resources) { - + eq = absl::make_unique(); + eq->init(config::defaultSampleRate); } void sfz::EQHolder::reset() { - eq.clear(); + eq->clear(); + prepared = false; } -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; - - // Setup the modulated values - lastFrequency = baseFrequency; - for (const auto& mod : description.frequencyCC) - lastFrequency += midiState.getCCValue(mod.cc) * mod.data; - lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); - - lastBandwidth = baseBandwidth; - for (const auto& mod : description.bandwidthCC) - lastBandwidth += midiState.getCCValue(mod.cc) * mod.data; - lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); - - lastGain = baseGain; - for (const auto& mod : description.gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); - - // Initialize the EQ - eq.prepare(lastFrequency, lastBandwidth, lastGain); + baseFrequency = description->frequency + velocity * description->vel2frequency; + baseBandwidth = description->bandwidth; + baseGain = description->gain + velocity * description->vel2gain; + + 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)); + + // Disables smoothing of the parameters on the first call + prepared = false; } void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numFrames) { - auto justCopy = [&]() { - 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 - lastFrequency = baseFrequency; - for (const auto& mod : description->frequencyCC) - lastFrequency += midiState.getCCValue(mod.cc) * mod.data; - lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); - - lastBandwidth = baseBandwidth; - for (const auto& mod : description->bandwidthCC) - lastBandwidth += midiState.getCCValue(mod.cc) * mod.data; - lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); - - lastGain = baseGain; - for (const auto& mod : description->gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); - - if (lastGain == 0.0f) { - justCopy(); + for (unsigned channelIdx = 0; channelIdx < eq->channels(); channelIdx++) + copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); 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; -} -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; - }); + ModMatrix& mm = resources.modMatrix; + auto frequencySpan = resources.bufferPool.getBuffer(numFrames); + auto bandwidthSpan = resources.bufferPool.getBuffer(numFrames); + auto gainSpan = resources.bufferPool.getBuffer(numFrames); - if (eq == eqs.end()) - return {}; - - (**eq).setup(description, numChannels, velocity); - return *eq; -} + if (!frequencySpan || !bandwidthSpan || !gainSpan) + return; -size_t sfz::EQPool::getActiveEQs() const -{ - return absl::c_count_if(eqs, [](const EQHolderPtr& holder) { - return holder.use_count() > 1; - }); -} + fill(*frequencySpan, baseFrequency); + if (float* mod = mm.getModulation(frequencyTarget)) + add(absl::Span(mod, numFrames), *frequencySpan); -size_t sfz::EQPool::setnumEQs(size_t numEQs) -{ - const std::lock_guard eqLock { eqGuard }; + fill(*bandwidthSpan, baseBandwidth); + if (float* mod = mm.getModulation(bandwidthTarget)) + add(absl::Span(mod, numFrames), *bandwidthSpan); - swapAndPopAll(eqs, [](sfz::EQHolderPtr& eq) { return eq.use_count() == 1; }); + fill(*gainSpan, baseGain); + if (float* mod = mm.getModulation(gainTarget)) + add(absl::Span(mod, numFrames), *gainSpan); - for (size_t i = eqs.size(); i < numEQs; ++i) { - eqs.emplace_back(std::make_shared(midiState)); - eqs.back()->setSampleRate(sampleRate); + if (!prepared) { + eq->prepare(frequencySpan->front(), bandwidthSpan->front(), gainSpan->front()); + prepared = true; } - return eqs.size(); + eq->processModulated( + inputs, + outputs, + frequencySpan->data(), + bandwidthSpan->data(), + gainSpan->data(), + numFrames + ); } -void sfz::EQPool::setSampleRate(float sampleRate) + +void sfz::EQHolder::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 7c70af224..30ce889a7 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -1,7 +1,8 @@ #pragma once #include "SfzFilter.h" -#include "EQDescription.h" -#include "MidiState.h" +#include "Region.h" +#include "Resources.h" +#include "utility/SpinMutex.h" #include #include #include @@ -13,15 +14,15 @@ class EQHolder { public: EQHolder() = delete; - EQHolder(const MidiState& state); + EQHolder(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 * @@ -30,94 +31,27 @@ class EQHolder * @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: + 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 }; + bool prepared { false }; + ModMatrix::TargetId gainTarget; + ModMatrix::TargetId frequencyTarget; + ModMatrix::TargetId bandwidthTarget; }; -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: - std::mutex eqGuard; - float sampleRate { config::defaultSampleRate }; - const MidiState& midiState; - std::vector eqs; -}; } diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index 26ee10aef..da99d98c9 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -15,7 +15,11 @@ #include "effects/Apan.h" #include "effects/Lofi.h" #include "effects/Limiter.h" +#include "effects/Compressor.h" +#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" @@ -31,7 +35,11 @@ void EffectFactory::registerStandardEffectTypes() registerEffectType("apan", fx::Apan::makeInstance); registerEffectType("lofi", fx::Lofi::makeInstance); 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); + registerEffectType("fverb", fx::Fverb::makeInstance); // extensions (book) registerEffectType("rectify", fx::Rectify::makeInstance); diff --git a/src/sfizz/FileInstrument.cpp b/src/sfizz/FileInstrument.cpp deleted file mode 100644 index 8f4885fe4..000000000 --- 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 1c75e988d..000000000 --- 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 000000000..60f6d64ed --- /dev/null +++ b/src/sfizz/FileMetadata.cpp @@ -0,0 +1,396 @@ +// 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: 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 +#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 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 = u32le(bytes); + 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 = u32be(bytes); + return true; +} + +//------------------------------------------------------------------------------ + +struct FileMetadataReader::Impl { + FILE_u stream_; + std::vector riffChunks_; + + 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() + : 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 +{ + return impl_->riffChunk(index); +} + +const RiffChunkInfo* FileMetadataReader::Impl::riffChunk(size_t index) const +{ + return (index < riffChunks_.size()) ? &riffChunks_[index] : nullptr; +} + +const RiffChunkInfo* FileMetadataReader::riffChunkById(RiffChunkId id) const +{ + return impl_->riffChunkById(id); +} + +const RiffChunkInfo* FileMetadataReader::Impl::riffChunkById(RiffChunkId id) const +{ + for (const RiffChunkInfo& riff : riffChunks_) { + if (riff.id == id) + return &riff; + } + return nullptr; +} + +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) + return 0; + + count = (count < riff->length) ? count : riff->length; + + FILE* stream = 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 false; + + 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 + std::lround(extractU32(0x18 - 8) * (100.0 / (static_cast(1) << 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; +} + +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; + + // 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.crossTableInterpolation = 0; + wt.oneShot = false; + + return true; +} + +} // namespace sfz diff --git a/src/sfizz/FileMetadata.h b/src/sfizz/FileMetadata.h new file mode 100644 index 000000000..9e93be2e3 --- /dev/null +++ b/src/sfizz/FileMetadata.h @@ -0,0 +1,94 @@ +// 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 +#if defined(_WIN32) +#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 +#include +#endif +#include + +namespace sfz { + +typedef std::array RiffChunkId; + +struct RiffChunkInfo { + size_t index; + off_t fileOffset; + RiffChunkId id; + 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_; +}; + +} // namespace sfz diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 9bec98ef5..056c76d66 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -24,7 +24,7 @@ // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "FilePool.h" -#include "FileInstrument.h" +#include "AudioReader.h" #include "Buffer.h" #include "AudioBuffer.h" #include "AudioSpan.h" @@ -45,104 +45,78 @@ #else #include #endif +#include "threadpool/ThreadPool.h" +using namespace std::placeholders; +static ThreadPool threadPool { std::thread::hardware_concurrency() > 2 ? std::thread::hardware_concurrency() - 2 : 1 }; -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) +sfz::FileAudioBuffer readFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor) { - auto baseBuffer = absl::make_unique(); - readBaseFile(sndFile, *baseBuffer, numFrames, reverse); + sfz::FileAudioBuffer baseBuffer; + readBaseFile(reader, baseBuffer, numFrames); if (factor == sfz::Oversampling::x1) return baseBuffer; - auto outputBuffer = absl::make_unique(sndFile.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; } -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) -: 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); + lastUsedFiles.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 @@ -152,7 +126,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); @@ -161,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; @@ -172,21 +146,26 @@ 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; } 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)) + 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; } @@ -199,7 +178,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 @@ -221,27 +200,40 @@ 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) { - DBG("[sfizz] Missing logic for " << sndFile.channels() << " channels, discarding sample " << fileId); + AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); + const unsigned channels = reader->channels(); + + if (channels != 1 && channels != 2) { + DBG("[sfizz] Missing logic for " << reader->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 {}; + bool haveInstrumentInfo = reader->getInstrument(&instrumentInfo); + + FileMetadataReader mdReader; + bool mdReaderOpened = mdReader.open(file); + + if (!haveInstrumentInfo) { + // if no instrument, then try extracting from embedded RIFF chunks (flac) + if (mdReaderOpened) + haveInstrumentInfo = mdReader.extractRiffInstrument(instrumentInfo); + } - const int sndFormat = sndFile.format(); - if ((sndFormat & SF_FORMAT_TYPEMASK) == SF_FORMAT_FLAC) - sfz::FileInstruments::extractFromFlac(file, instrumentInfo); - else - sndFile.command(SFC_GET_INSTRUMENT, &instrumentInfo, sizeof(instrumentInfo)); + if (mdReaderOpened) { + WavetableInfo wt; + if (mdReader.extractWavetableInfo(wt)) + returnedValue.wavetable = wt; + } 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); } @@ -250,6 +242,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; } @@ -259,13 +254,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() }; - 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) + if (loadInRam) return frames; else return min(frames, maxOffset + preloadSize); @@ -273,209 +268,178 @@ 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()); + 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(sndFile.samplerate()); - FileDataHandle handle { - readFromFile(sndFile, framesToLoad, oversamplingFactor, fileId.isReverse()), + 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) 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; + return { &existingFile->second }; } else { - fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(sndFile.samplerate()); - FileDataHandle handle { - readFromFile(sndFile, frames, oversamplingFactor, fileId.isReverse()), + 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 { + 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() }; - SndfileHandle sndFile(file.string().c_str()); - preloadedFile.second.preloadedData = readFromFile(sndFile, preloadSize + maxOffset, oversamplingFactor, preloadedFile.first.isReverse()); - } - this->preloadSize = preloadSize; -} - -void sfz::FilePool::tryToClearPromises() -{ - const std::lock_guard promiseLock { promiseGuard }; - - for (auto& promise: promisesToClear) { - if (promise->dataStatus != FilePromise::DataStatus::Wait) - promise->reset(); + AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); + preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, oversamplingFactor); } } -void sfz::FilePool::clearingThread() +void sfz::FilePool::loadingJob(QueuedFileData data) noexcept { raiseCurrentThreadPriority(); - RTSemaphore& request = semClearingRequest; - do { - request.wait(); - if (quitThread) - return; - tryToClearPromises(); - } while (1); -} - -void sfz::FilePool::loadingThread() noexcept -{ - raiseCurrentThreadPriority(); + 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); - FilePromisePtr promise; - do { - workerBarrier.wait(); + if (readError) { + DBG("[sfizz] libsndfile errored for " << data.id << " with message " << readError.message()); + return; + } - if (emptyQueue) { - while (promiseQueue.try_pop(promise)) { - // We're just dequeuing - } - emptyQueue = false; - semEmptyQueueFinished.post(); - continue; - } + FileData::Status currentStatus = data.data->status.load(); - if (quitThread) + unsigned spinCounter { 0 }; + while (currentStatus == FileData::Status::Invalid) { + // Spin until the state changes + if (spinCounter > 1024) { + DBG("[sfizz] " << data.id << " is stuck on Invalid? Leaving the load"); return; - - if (!promiseQueue.try_pop(promise)) { - continue; } - threadsLoading++; - const auto loadStartTime = std::chrono::high_resolution_clock::now(); - const auto waitDuration = loadStartTime - promise->creationTime; + std::this_thread::sleep_for(std::chrono::microseconds(100)); + currentStatus = data.data->status.load(); + spinCounter += 1; + } - 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()); - promise->dataStatus = FilePromise::DataStatus::Error; - continue; - } - const auto frames = static_cast(sndFile.frames()); - streamFromFile(sndFile, frames, oversamplingFactor, promise->fileId.isReverse(), 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()); + // Already loading or loaded + if (currentStatus != FileData::Status::Preloaded) + return; + + // Someone else got the token + if (!data.data->status.compare_exchange_strong(currentStatus, FileData::Status::Streaming)) + return; - threadsLoading--; + 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()); - semFilledPromiseQueueAvailable.wait(); - filledPromiseQueue.push(promise); + data.data->status = FileData::Status::Done; - 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 { 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() }; - SndfileHandle sndFile(file.string().c_str()); - preloadedFile.second.preloadedData = readFromFile(sndFile, preloadSize + maxOffset, factor, preloadedFile.first.isReverse()); - preloadedFile.second.information.sampleRate *= samplerateChange; + AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); + preloadedFile.second.preloadedData = readFromFile(*reader, framesToLoad, factor); + 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; @@ -491,28 +455,76 @@ uint32_t sfz::FilePool::getPreloadSize() const noexcept return preloadSize; } -void sfz::FilePool::emptyFileLoadingQueues() noexcept +template +bool is_ready(std::future const& f) { - emptyQueue = true; - workerBarrier.post(); - semEmptyQueueFinished.wait(); + return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready; } -void sfz::FilePool::waitForBackgroundLoading() noexcept +void sfz::FilePool::dispatchingJob() 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)); + QueuedFileData queuedData; + while (dispatchFlag) { + dispatchBarrier.wait(); + + if (emptyQueueFlag) { + while (filesToLoad.try_pop(queuedData)) { + // pass + } + semEmptyQueueFinished.post(); + emptyQueueFlag = false; + 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 + swapAndPopAll(loadingJobs, [](std::future& future) { + return is_ready(future); + }); } +} - // Spinlocking on the threads possibly logging in the background - while (threadsLoading > 0) { - std::this_thread::sleep_for(std::chrono::microseconds(100)); +void sfz::FilePool::garbageJob() noexcept +{ + while (garbageFlag) { + semGarbageBarrier.wait(); + { + std::lock_guard guard { garbageMutex }; + for (auto& g: garbageToCollect) + g.reset(); + + garbageToCollect.clear(); + } } } +void sfz::FilePool::emptyFileLoadingQueues() noexcept +{ + ASSERT(dispatchFlag); + emptyQueueFlag = true; + std::error_code ec; + dispatchBarrier.post(ec); + + if (!ec) + semEmptyQueueFinished.wait(); +} + +void sfz::FilePool::waitForBackgroundLoading() noexcept +{ + std::lock_guard guard { loadingJobsMutex }; + + for (auto& job : loadingJobs) + job.wait(); + + loadingJobs.clear(); +} + void sfz::FilePool::raiseCurrentThreadPriority() noexcept { #if defined(_WIN32) @@ -535,8 +547,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"); @@ -544,3 +555,62 @@ 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); + } +} + +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 2547591fc..a44ce9483 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -31,7 +31,9 @@ #include "AudioBuffer.h" #include "AudioSpan.h" #include "FileId.h" +#include "FileMetadata.h" #include "SIMDHelpers.h" +#include "utility/SpinMutex.h" #include "ghc/fs_std.hpp" #include #include @@ -40,7 +42,8 @@ #include "Logger.h" #include #include -#include +#include +#include "utility/SpinMutex.h" namespace sfz { using FileAudioBuffer = AudioBuffer; 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 }; double sampleRate { config::defaultSampleRate }; int numChannels { 0 }; + int rootKey { 0 }; + absl::optional wavetable; }; // Strict C++11 disallows member initialization if aggregate initialization is to be used... -struct FileDataHandle +struct FileData { - FileAudioBufferPtr preloadedData; - FileInformation information; -}; + enum class Status { Invalid, Preloaded, Streaming, Done }; + FileData() = default; + FileData(FileAudioBuffer preloaded, FileInformation info) + : preloadedData(std::move(preloaded)), information(std::move(info)) + { -struct FilePromise -{ + } AudioSpan 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) = delete; + FileData& operator=(const FileData& other) = delete; + FileData(FileData&& other) + { + 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(); + } + FileData& operator=(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(); + return *this; } - enum class DataStatus { - Wait = 0, - Ready, - Error, - }; + FileAudioBuffer preloadedData; + FileInformation information; + FileAudioBuffer fileData {}; + std::atomic status { Status::Invalid }; + std::atomic availableFrames { 0 }; + std::atomic readerCount { 0 }; + std::chrono::time_point lastViewerLeftAt; - bool waiting() const { return dataStatus == DataStatus::Wait; } + LEAK_DETECTOR(FileData); +}; - void sleepUntilComplete() + +class FileDataHolder { +public: + FileDataHolder() = default; + FileDataHolder(const FileDataHolder&) = delete; + FileDataHolder& operator=(const FileDataHolder&) = delete; + FileDataHolder(FileDataHolder&& other) { - while (waiting()) - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + 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; - FileId fileId {}; - FileAudioBufferPtr preloadedData {}; - FileAudioBuffer fileData {}; - float sampleRate { config::defaultSampleRate }; - Oversampling oversamplingFactor { config::defaultOversamplingFactor }; - std::atomic availableFrames { 0 }; - std::atomic dataStatus { DataStatus::Wait }; - std::chrono::time_point creationTime; + data->readerCount += 1; + } + void reset() + { + if (!data) + return; - LEAK_DETECTOR(FilePromise); + 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 @@ -182,7 +231,7 @@ class FilePool { * @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. @@ -208,19 +257,12 @@ class FilePool { */ 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. @@ -264,36 +306,67 @@ class FilePool { * 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; + /** + * @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 }; + volatile bool dispatchFlag { true }; + volatile bool garbageFlag { true }; + volatile bool emptyQueueFlag { false }; + RTSemaphore dispatchBarrier; RTSemaphore semEmptyQueueFinished; - std::atomic threadsLoading { 0 }; - RTSemaphore workerBarrier; - RTSemaphore semClearingRequest; + RTSemaphore semGarbageBarrier; + + // Structures for the background loaders + struct QueuedFileData + { + 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; + void garbageJob() noexcept; + void loadingJob(QueuedFileData data) noexcept; + std::mutex loadingJobsMutex; + std::vector> loadingJobs; + std::thread dispatchThread { &FilePool::dispatchingJob, this }; + std::thread garbageThread { &FilePool::garbageJob, this }; - // File promises data structures along with their guards. - std::vector emptyPromises; - std::vector temporaryFilePromises; - std::vector promisesToClear; - std::mutex promiseGuard; + 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/FilterDescription.h b/src/sfizz/FilterDescription.h index b31e81e95..2f3193a0c 100644 --- a/src/sfizz/FilterDescription.h +++ b/src/sfizz/FilterDescription.h @@ -20,10 +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 }; - CCMap cutoffCC { Default::filterCutoffCC }; - CCMap resonanceCC { Default::filterResonanceCC }; - CCMap gainCC { Default::filterGainCC }; }; } diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 164b3ea82..d8c16e401 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -5,153 +5,103 @@ #include #include -sfz::FilterHolder::FilterHolder(const MidiState& midiState) -: midiState(midiState) +sfz::FilterHolder::FilterHolder(Resources& resources) +: resources(resources) { - + filter = absl::make_unique(); + filter->init(config::defaultSampleRate); } void sfz::FilterHolder::reset() { - filter.clear(); + filter->clear(); + prepared = false; } -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 *= centsFactor(dist(Random::randomGenerator)); + baseCutoff = description->cutoff; + if (description->random != 0) { + fast_real_distribution dist { -description->random, 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; - - // Setup the modulated values - lastCutoff = baseCutoff; - for (const auto& mod : description.cutoffCC) - lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data); - lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); + baseGain = description->gain; + baseResonance = description->resonance; - lastResonance = baseResonance; - for (const auto& mod : description.resonanceCC) - lastResonance += midiState.getCCValue(mod.cc) * mod.data; - lastResonance = Default::filterResonanceRange.clamp(lastResonance); + 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)); - lastGain = baseGain; - for (const auto& mod : description.gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); - - // 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++) + for (unsigned channelIdx = 0; channelIdx < filter->channels(); channelIdx++) copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); 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? - lastCutoff = baseCutoff; - for (const auto& mod : description->cutoffCC) - lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data); - lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); - - lastResonance = baseResonance; - for (const auto& mod : description->resonanceCC) - lastResonance += midiState.getCCValue(mod.cc) * mod.data; - lastResonance = Default::filterResonanceRange.clamp(lastResonance); - - lastGain = baseGain; - for (const auto& mod : description->gainCC) - lastGain += midiState.getCCValue(mod.cc) * mod.data; - lastGain = Default::filterGainRange.clamp(lastGain); - - 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 {}; + ModMatrix& mm = resources.modMatrix; + auto cutoffSpan = resources.bufferPool.getBuffer(numFrames); + auto resonanceSpan = resources.bufferPool.getBuffer(numFrames); + auto gainSpan = resources.bufferPool.getBuffer(numFrames); - 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; -} + if (!cutoffSpan || !resonanceSpan || !gainSpan) + return; -size_t sfz::FilterPool::getActiveFilters() const -{ - return absl::c_count_if(filters, [](const FilterHolderPtr& holder) { - return holder.use_count() > 1; - }); -} + fill(*cutoffSpan, baseCutoff); + if (float* mod = mm.getModulation(cutoffTarget)) { + for (size_t i = 0; i < numFrames; ++i) + (*cutoffSpan)[i] *= centsFactor(mod[i]); + } + sfz::clampAll(*cutoffSpan, Default::filterCutoffRange); -size_t sfz::FilterPool::setNumFilters(size_t numFilters) -{ - const std::lock_guard filterLock { filterGuard }; + fill(*resonanceSpan, baseResonance); + if (float* mod = mm.getModulation(resonanceTarget)) + add(absl::Span(mod, numFrames), *resonanceSpan); - swapAndPopAll(filters, [](sfz::FilterHolderPtr& filter) { return filter.use_count() == 1; }); + fill(*gainSpan, baseGain); + if (float* mod = mm.getModulation(gainTarget)) + add(absl::Span(mod, numFrames), *gainSpan); - for (size_t i = filters.size(); i < numFilters; ++i) { - filters.emplace_back(std::make_shared(midiState)); - filters.back()->setSampleRate(sampleRate); + if (!prepared) { + filter->prepare(cutoffSpan->front(), resonanceSpan->front(), gainSpan->front()); + prepared = true; } - return filters.size(); + filter->processModulated( + inputs, + outputs, + cutoffSpan->data(), + resonanceSpan->data(), + gainSpan->data(), + numFrames + ); } -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 36db8f514..76074a0c1 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -1,8 +1,9 @@ #pragma once #include "SfzFilter.h" -#include "FilterDescription.h" -#include "MidiState.h" +#include "Region.h" +#include "Resources.h" #include "Defaults.h" +#include "utility/SpinMutex.h" #include #include #include @@ -14,16 +15,16 @@ class FilterHolder { public: FilterHolder() = delete; - FilterHolder(const MidiState& state); + FilterHolder(Resources& resources); /** * @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 * @@ -32,97 +33,27 @@ class FilterHolder * @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: + 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 }; - using filterRandomDist = std::uniform_int_distribution; - filterRandomDist dist { 0, sfz::Default::filterRandom }; + ModMatrix::TargetId gainTarget; + ModMatrix::TargetId cutoffTarget; + ModMatrix::TargetId resonanceTarget; + bool prepared { false }; }; -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: - std::mutex filterGuard; - float sampleRate { config::defaultSampleRate }; - const MidiState& midiState; - std::vector filters; -}; } diff --git a/src/sfizz/FlexEGDescription.cpp b/src/sfizz/FlexEGDescription.cpp new file mode 100644 index 000000000..18ea92e36 --- /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 000000000..b8a0f59bd --- /dev/null +++ b/src/sfizz/FlexEGDescription.h @@ -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 + +#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; + // 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 new file mode 100644 index 000000000..3a7402dd5 --- /dev/null +++ b/src/sfizz/FlexEnvelope.cpp @@ -0,0 +1,241 @@ +// 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 +- [ ] egN_loop +- [ ] egN_loop_shape +- [ ] egN_loop_count +*/ + +#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; +} + +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_; + 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; + + while (frameIndex < numFrames) { + // Check for release + if (currentFramesUntilRelease_ && *currentFramesUntilRelease_ == 0) { + isReleased_ = true; + currentFramesUntilRelease_ = absl::nullopt; + } + + // Perform stage transitions + 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); + return; + } + } + + // Process without going past the release point, if there is one + size_t maxFrameIndex = numFrames; + if (currentFramesUntilRelease_) + maxFrameIndex = std::min(maxFrameIndex, frameIndex + *currentFramesUntilRelease_); + + // 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) { + time += samplePeriod; + float x = time * (1.0f / stageEndTime); + float c = curve.evalNormalized(x); + level = sourceLevel + c * (targetLevel - sourceLevel); + out[frameIndex++] = level; + ++framesDone; + } + currentLevel_ = level; + + // Update the counter to release + if (currentFramesUntilRelease_) + *currentFramesUntilRelease_ -= framesDone; + + currentTime_ = time; + } +} + +bool FlexEnvelope::Impl::advanceToNextStage() +{ + const FlexEGDescription& desc = *desc_; + + unsigned nextStageNo = currentStageNumber_ + 1; + currentStageNumber_ = nextStageNo; + + if (nextStageNo >= desc.points.size()) + return false; + + const FlexEGPoint& point = desc.points[nextStageNo]; + 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 000000000..d1d01a4a6 --- /dev/null +++ b/src/sfizz/FlexEnvelope.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 +#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); + + /** + 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. + */ + void process(absl::Span out); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/LFO.cpp b/src/sfizz/LFO.cpp new file mode 100644 index 000000000..06c823b54 --- /dev/null +++ b/src/sfizz/LFO.cpp @@ -0,0 +1,309 @@ +// 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 fadePosition_ = 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(unsigned triggerDelay) +{ + 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; + 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; +} + +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(); + + 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]; + + 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(); + + 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; + + // 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; + + 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]; + + 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; + } + } + + 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; + const float fadeStep = samplePeriod / fadeTime; + + for (size_t i = 0; i < numFrames && fadePosition < 1; ++i) { + out[i] *= fadePosition; + fadePosition = std::min(1.0f, fadePosition + fadeStep); + } + + impl.fadePosition_ = fadePosition; +} + +} // namespace sfz diff --git a/src/sfizz/LFO.h b/src/sfizz/LFO.h new file mode 100644 index 000000000..7a43fd134 --- /dev/null +++ b/src/sfizz/LFO.h @@ -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 + +#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 // instantiate 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(unsigned triggerDelay); + + /** + 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); + + /** + Process the fade in gain, and apply it to the buffer. + */ + void processFadeIn(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 000000000..4e846ef23 --- /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 000000000..8f5344c97 --- /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/MathHelpers.h b/src/sfizz/MathHelpers.h index f0dbebc44..50b012bd6 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) { @@ -274,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 */ @@ -660,7 +685,7 @@ class fast_gaussian_generator { } private: - std::array seeds_ {}; + std::array seeds_ {{}}; float mean_ { 0 }; float gain_ { 0 }; }; diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 3cb0a9ff5..03e65a9e1 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 diff --git a/src/sfizz/ModifierHelpers.h b/src/sfizz/ModifierHelpers.h index 5ea57c022..e9b763610 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 4868d1126..000000000 --- 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.cpp b/src/sfizz/Opcode.cpp index f72619411..ff6cbc6d3 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -9,10 +9,13 @@ #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" +#include #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)) @@ -48,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); @@ -94,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; @@ -114,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); @@ -154,3 +157,148 @@ 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) +{ + // Cakewalk-style booleans, case-insensitive + if (absl::EqualsIgnoreCase(opcode.value, "off")) + return false; + if (absl::EqualsIgnoreCase(opcode.value, "on")) + return true; + + // 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 +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); /*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) +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 cd31c8c67..a6c3a1d63 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -13,9 +13,10 @@ #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 // charconv support is still sketchy with clang/gcc so we use abseil's numbers #include "absl/strings/numbers.h" @@ -42,7 +43,7 @@ enum OpcodeCategory { */ enum OpcodeScope { //! unknown scope or other - kOpcodeScopeGeneric, + kOpcodeScopeGeneric = 0, //! global scope kOpcodeScopeGlobal, //! control scope @@ -124,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 @@ -158,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 @@ -207,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 @@ -226,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 @@ -245,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 @@ -264,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. @@ -282,13 +202,8 @@ 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); } + +std::ostream &operator<<(std::ostream &os, const sfz::Opcode &opcode); diff --git a/src/sfizz/OpcodeCleanup.cpp b/src/sfizz/OpcodeCleanup.cpp index 863a6eb07..459ce5144 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 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 @@ -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]; @@ -174,11 +174,12 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc //-------------------------------------------------------------------------- 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 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yy19: ++YYCURSOR; yy20: -#line 168 "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) { @@ -244,64 +245,65 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc 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 +312,13 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc 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,96 +333,127 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc 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 'o': goto yy71; + default: goto yy34; + } +yy53: + 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 yy53; + case '_': goto yy72; + default: goto yy34; + } +yy55: yych = *++YYCURSOR; switch (yych) { case '0': @@ -432,11 +465,11 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy52; - case '_': goto yy69; + case '9': goto yy55; + case '_': goto yy73; default: goto yy34; } -yy54: +yy57: yych = *++YYCURSOR; switch (yych) { case '0': @@ -450,26 +483,26 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt1 = YYCURSOR; - goto yy70; - case '_': goto yy72; - case 'e': goto yy65; - case 'l': goto yy66; - case 't': goto yy73; + goto yy74; + case '_': goto yy76; + case 'e': goto yy68; + case 'l': goto yy69; + case 't': goto yy77; default: goto yy34; } -yy55: +yy58: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy74; + case 'n': goto yy78; default: goto yy34; } -yy56: +yy59: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy75; + case 'e': goto yy79; default: goto yy34; } -yy57: +yy60: yych = *++YYCURSOR; switch (yych) { case '0': @@ -481,101 +514,112 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy76; + case '9': goto yy80; default: goto yy34; } -yy58: +yy61: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy78; + case 'p': goto yy82; default: goto yy34; } -yy59: +yy62: yych = *++YYCURSOR; switch (yych) { case 'b': yyt1 = YYCURSOR; - goto yy79; + goto yy83; case 'm': yyt1 = YYCURSOR; - goto yy80; + goto yy84; default: goto yy34; } -yy60: +yy63: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy81; - case 'l': goto yy82; + case 'h': goto yy85; + case 'l': goto yy86; default: goto yy34; } -yy61: +yy64: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy83; + case 'c': goto yy87; default: goto yy34; } -yy62: +yy65: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy84; + case 'y': goto yy88; default: goto yy34; } -yy63: +yy66: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy85; + case 'o': goto yy89; default: goto yy34; } -yy64: +yy67: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy86; + case 'e': goto yy90; default: goto yy34; } -yy65: +yy68: yych = *++YYCURSOR; switch (yych) { - case 'g': goto yy87; + case 'g': goto yy91; default: goto yy34; } -yy66: +yy69: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy88; + case 'f': goto yy92; default: goto yy34; } -yy67: +yy70: yych = *++YYCURSOR; switch (yych) { case 'd': yyt1 = YYCURSOR; - goto yy89; + goto yy93; case 'u': yyt1 = YYCURSOR; - goto yy90; + goto yy94; default: goto yy34; } -yy68: +yy71: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy91; + case 'f': goto yy95; default: goto yy34; } -yy69: +yy72: + yych = *++YYCURSOR; + switch (yych) { + case 'c': + yyt2 = YYCURSOR; + goto yy96; + case 'r': + yyt2 = YYCURSOR; + goto yy97; + default: goto yy34; + } +yy73: yych = *++YYCURSOR; switch (yych) { case 'b': yyt2 = YYCURSOR; - goto yy92; + goto yy98; case 'f': yyt2 = YYCURSOR; - goto yy93; + goto yy99; case 'g': yyt2 = YYCURSOR; - goto yy94; + goto yy100; default: goto yy34; } -yy70: +yy74: yych = *++YYCURSOR; switch (yych) { case '0': @@ -587,38 +631,38 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy70; - case 't': goto yy95; + case '9': goto yy74; + case 't': goto yy101; default: goto yy34; } -yy72: +yy76: yych = *++YYCURSOR; yyt1 = YYCURSOR; - goto yy99; -yy73: + goto yy105; +yy77: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy100; + case 'y': goto yy106; default: goto yy34; } -yy74: +yy78: yych = *++YYCURSOR; switch (yych) { case 0x00: yyt2 = yyt3 = NULL; - goto yy101; + goto yy107; case '_': yyt3 = YYCURSOR; - goto yy103; + goto yy109; default: goto yy34; } -yy75: +yy79: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy105; + case 'a': goto yy110; default: goto yy34; } -yy76: +yy80: yych = *++YYCURSOR; switch (yych) { case '0': @@ -630,589 +674,696 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy76; - case '_': goto yy106; + case '9': goto yy80; + case '_': goto yy111; default: goto yy34; } -yy78: +yy82: yych = *++YYCURSOR; switch (yych) { case 'e': yyt1 = YYCURSOR; - goto yy107; + goto yy112; case 'm': yyt1 = YYCURSOR; - goto yy108; + goto yy113; case 's': yyt1 = YYCURSOR; - goto yy109; + goto yy114; default: goto yy34; } -yy79: +yy83: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy110; + case 'y': goto yy115; default: goto yy34; } -yy80: +yy84: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy111; + case 'o': goto yy116; default: goto yy34; } -yy81: +yy85: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy112; + case 'i': goto yy117; default: goto yy34; } -yy82: +yy86: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy112; + case 'o': goto yy117; default: goto yy34; } -yy83: +yy87: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy49; + case 'h': goto yy50; default: goto yy34; } -yy84: +yy88: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy113; + case 'p': goto yy118; default: goto yy34; } -yy85: +yy89: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy114; + case 'n': goto yy119; default: goto yy34; } -yy86: +yy90: yych = *++YYCURSOR; switch (yych) { case 0x00: yyt2 = yyt3 = NULL; - goto yy115; + goto yy120; case '_': yyt3 = YYCURSOR; - goto yy117; + goto yy122; default: goto yy34; } -yy87: +yy91: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy119; + case '_': goto yy124; default: goto yy34; } -yy88: +yy92: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy120; + case 'o': goto yy125; default: goto yy34; } -yy89: +yy93: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy121; + case 'o': goto yy126; default: goto yy34; } -yy90: +yy94: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy122; + case 'p': goto yy127; default: goto yy34; } -yy91: +yy95: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy123; + case 'f': goto yy128; default: goto yy34; } -yy92: +yy96: yych = *++YYCURSOR; switch (yych) { - case 'w': goto yy124; + case 'u': goto yy129; default: goto yy34; } -yy93: +yy97: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy125; + case 'e': goto yy130; default: goto yy34; } -yy94: +yy98: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy126; + case 'w': goto yy131; default: goto yy34; } -yy95: +yy99: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy127; + case 'r': goto yy132; default: goto yy34; } -yy96: +yy100: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy133; + default: goto yy34; + } +yy101: + yych = *++YYCURSOR; + switch (yych) { + case 'y': goto yy134; + 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 169 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil1_", group(1)); goto end_region; } -#line 771 "src/sfizz/OpcodeCleanup.cpp" -yy98: +#line 827 "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 yy135; 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 146 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("volume", group(1)); goto end_region; } -#line 795 "src/sfizz/OpcodeCleanup.cpp" -yy103: +#line 851 "src/sfizz/OpcodeCleanup.cpp" +yy109: yych = *++YYCURSOR; - if (yych <= 0x00) { - yyt2 = YYCURSOR; - goto yy101; + switch (yych) { + case 'r': goto yy138; + default: goto yy137; } - goto yy103; -yy105: +yy110: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy129; + case 'l': goto yy139; default: goto yy34; } -yy106: +yy111: yych = *++YYCURSOR; switch (yych) { + case 'c': + yyt2 = YYCURSOR; + goto yy140; case 'o': yyt2 = YYCURSOR; - goto yy130; + goto yy141; case 'r': yyt2 = YYCURSOR; - goto yy131; + goto yy142; case 's': yyt2 = YYCURSOR; - goto yy132; + goto yy143; case 'w': yyt2 = YYCURSOR; - goto yy133; + goto yy144; default: goto yy34; } -yy107: +yy112: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy134; + case 'n': goto yy145; default: goto yy34; } -yy108: +yy113: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy135; + case 'o': goto yy146; default: goto yy34; } -yy109: +yy114: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy136; + case 't': goto yy147; default: goto yy34; } -yy110: +yy115: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy137; + if (yych <= 0x00) goto yy148; goto yy34; -yy111: +yy116: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy139; + case 'd': goto yy150; default: goto yy34; } -yy112: +yy117: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy140; - case 'h': goto yy141; + case 'c': goto yy151; + case 'h': goto yy152; default: goto yy34; } -yy113: +yy118: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy142; + case 'h': goto yy153; default: goto yy34; } -yy114: +yy119: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy143; + case 'a': goto yy154; default: goto yy34; } -yy115: +yy120: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 136 "src/sfizz/OpcodeCleanup.re" +#line 150 "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" +yy122: yych = *++YYCURSOR; if (yych <= 0x00) { yyt2 = YYCURSOR; - goto yy115; + goto yy120; } - goto yy117; -yy119: + goto yy122; +yy124: yych = *++YYCURSOR; switch (yych) { case 'a': yyt2 = YYCURSOR; - goto yy144; + goto yy155; case 'd': yyt2 = YYCURSOR; - goto yy145; + goto yy156; case 'h': yyt2 = YYCURSOR; - goto yy146; + goto yy157; case 'r': yyt2 = YYCURSOR; - goto yy147; + goto yy158; case 's': yyt2 = YYCURSOR; - goto yy148; + goto yy159; default: goto yy34; } -yy120: +yy125: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy149; + case '_': goto yy160; default: goto yy34; } -yy121: +yy126: yych = *++YYCURSOR; switch (yych) { - case 'w': goto yy150; + case 'w': goto yy161; default: goto yy34; } -yy122: +yy127: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy151; + if (yych <= 0x00) goto yy162; goto yy34; -yy123: +yy128: yych = *++YYCURSOR; switch (yych) { case 0x00: yyt2 = yyt3 = NULL; - goto yy153; + 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 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; - default: goto yy34; - } -yy126: - yych = *++YYCURSOR; - switch (yych) { - case 'i': goto yy159; - default: goto yy34; - } -yy127: - yych = *++YYCURSOR; - switch (yych) { - case 'p': goto yy160; - default: goto yy34; - } -yy128: - yych = *++YYCURSOR; - switch (yych) { - case 'e': goto yy161; + goto yy168; default: goto yy34; } yy129: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy162; + case 't': goto yy169; default: goto yy34; } yy130: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy163; + case 's': goto yy170; default: goto yy34; } yy131: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy164; + case 'c': goto yy171; default: goto yy34; } yy132: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy165; + case 'e': goto yy172; default: goto yy34; } yy133: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy166; + case 'i': goto yy173; default: goto yy34; } yy134: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy167; + case 'p': goto yy174; default: goto yy34; } yy135: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy168; + case 'e': goto yy175; default: goto yy34; } yy136: yych = *++YYCURSOR; +yy137: + if (yych <= 0x00) { + yyt2 = YYCURSOR; + goto yy107; + } + goto yy136; +yy138: + yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy169; - default: goto yy34; + case 'a': goto yy176; + default: goto yy137; } -yy137: - ++YYCURSOR; - yynmatch = 2; - yypmatch[2] = yyt1; - yypmatch[0] = yyt1 - 3; - yypmatch[1] = YYCURSOR; - yypmatch[3] = YYCURSOR - 1; -#line 110 "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; + case 'c': goto yy177; default: goto yy34; } yy140: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy170; + case 'u': goto yy178; default: goto yy34; } yy141: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy171; + case 'f': goto yy179; default: goto yy34; } yy142: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy172; + case 'a': goto yy180; + case 'e': goto yy181; default: goto yy34; } yy143: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy173; + case 'c': goto yy182; default: goto yy34; } yy144: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy174; + case 'a': goto yy183; default: goto yy34; } yy145: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy175; + case 'd': goto yy184; default: goto yy34; } yy146: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy176; + case 'd': goto yy185; default: goto yy34; } yy147: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy177; + 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 119 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat("off_", group(1)); + goto end_region; + } +#line 1134 "src/sfizz/OpcodeCleanup.cpp" +yy150: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy178; - case 'u': goto yy179; + case 'e': goto yy115; default: goto yy34; } -yy149: +yy151: yych = *++YYCURSOR; switch (yych) { - case 'd': - yyt2 = YYCURSOR; - goto yy180; - case 'f': - yyt2 = YYCURSOR; - goto yy181; + case 'c': goto yy187; default: goto yy34; } -yy150: +yy152: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy122; + case 'd': goto yy188; default: goto yy34; } -yy151: +yy153: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy189; + default: goto yy34; + } +yy154: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy190; + default: goto yy34; + } +yy155: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy191; + default: goto yy34; + } +yy156: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy192; + default: goto yy34; + } +yy157: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy193; + default: goto yy34; + } +yy158: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy194; + default: goto yy34; + } +yy159: + yych = *++YYCURSOR; + switch (yych) { + 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 114 "src/sfizz/OpcodeCleanup.re" +#line 123 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("bend_", group(1)); goto end_region; } -#line 1121 "src/sfizz/OpcodeCleanup.cpp" -yy153: +#line 1225 "src/sfizz/OpcodeCleanup.cpp" +yy164: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 154 "src/sfizz/OpcodeCleanup.re" +#line 173 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("cutoff1", group(1)); goto end_region; } -#line 1134 "src/sfizz/OpcodeCleanup.cpp" -yy155: +#line 1238 "src/sfizz/OpcodeCleanup.cpp" +yy166: yych = *++YYCURSOR; - if (yych <= 0x00) { - yyt2 = YYCURSOR; - goto yy153; + switch (yych) { + 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; } - goto yy155; -yy157: +yy168: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy182; + case 'r': goto yy202; + default: goto yy201; + } +yy169: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy203; default: goto yy34; } -yy158: +yy170: yych = *++YYCURSOR; switch (yych) { - case 'q': goto yy124; + case 'o': goto yy204; default: goto yy34; } -yy159: +yy171: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy124; + case 'c': goto yy205; default: goto yy34; } -yy160: +yy172: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy183; + case 'q': goto yy131; default: goto yy34; } -yy161: +yy173: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy184; + switch (yych) { + case 'n': goto yy131; + default: goto yy34; + } +yy174: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy206; + default: goto yy34; + } +yy175: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy207; goto yy34; -yy162: +yy176: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy209; + default: goto yy137; + } +yy177: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy186; + case 'c': goto yy210; default: goto yy34; } -yy163: +yy178: yych = *++YYCURSOR; switch (yych) { - case 'f': goto yy187; + case 't': goto yy211; default: goto yy34; } -yy164: +yy179: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy188; + case 'f': goto yy212; default: goto yy34; } -yy165: +yy180: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy189; + case 't': goto yy213; default: goto yy34; } -yy166: +yy181: yych = *++YYCURSOR; switch (yych) { - case 'v': goto yy190; + case 's': goto yy214; default: goto yy34; } -yy167: +yy182: + yych = *++YYCURSOR; + switch (yych) { + 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 yy191; + if (yych <= 0x00) goto yy217; goto yy34; -yy168: +yy185: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy167; + case 'e': goto yy184; default: goto yy34; } -yy169: +yy186: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy193; + case 'r': goto yy219; default: goto yy34; } -yy170: +yy187: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1226,78 +1377,110 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt1 = YYCURSOR; - goto yy194; + goto yy220; default: goto yy34; } -yy171: +yy188: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy196; + case 'c': goto yy222; default: goto yy34; } -yy172: +yy189: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy197; + case 'n': goto yy223; default: goto yy34; } -yy173: +yy190: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy198; + case 'c': goto yy224; default: goto yy34; } -yy174: +yy191: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy199; + case 't': goto yy225; default: goto yy34; } -yy175: +yy192: yych = *++YYCURSOR; switch (yych) { case 'c': - case 'l': goto yy200; + case 'l': goto yy226; default: goto yy34; } -yy176: +yy193: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy201; + case 'l': goto yy227; default: goto yy34; } -yy177: +yy194: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy202; + case 'l': goto yy228; default: goto yy34; } -yy178: +yy195: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy203; + case 'a': goto yy229; default: goto yy34; } -yy179: +yy196: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy204; + case 's': goto yy230; default: goto yy34; } -yy180: +yy197: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy205; + case 'e': goto yy231; default: goto yy34; } -yy181: +yy198: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy206; - case 'r': goto yy207; + case 'a': goto yy232; + case 'r': goto yy233; default: goto yy34; } -yy182: +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': @@ -1311,25 +1494,31 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt3 = YYCURSOR; - goto yy208; + goto yy238; default: goto yy34; } -yy183: +yy206: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy210; + if (yych <= 0x00) goto yy240; goto yy34; -yy184: +yy207: ++YYCURSOR; yynmatch = 1; yypmatch[0] = YYCURSOR - 8; yypmatch[1] = YYCURSOR; -#line 118 "src/sfizz/OpcodeCleanup.re" +#line 127 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil1_type"); goto end_region; } -#line 1332 "src/sfizz/OpcodeCleanup.cpp" -yy186: +#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': @@ -1343,56 +1532,68 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt1 = YYCURSOR; - goto yy212; + goto yy243; default: goto yy34; } -yy187: +yy211: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy214; + case 'o': goto yy245; default: goto yy34; } -yy188: +yy212: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy215; + case 's': goto yy246; default: goto yy34; } -yy189: +yy213: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy190; + case 'i': goto yy247; default: goto yy34; } -yy190: +yy214: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy216; + case 'o': goto yy248; default: goto yy34; } -yy191: +yy215: + yych = *++YYCURSOR; + switch (yych) { + case 'l': goto yy216; + default: goto yy34; + } +yy216: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy249; + default: goto yy34; + } +yy217: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 4; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 106 "src/sfizz/OpcodeCleanup.re" +#line 115 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("loop_", group(1)); goto end_region; } -#line 1386 "src/sfizz/OpcodeCleanup.cpp" -yy193: +#line 1587 "src/sfizz/OpcodeCleanup.cpp" +yy219: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy167; + case 't': goto yy184; default: goto yy34; } -yy194: +yy220: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy217; + case 0x00: goto yy250; case '0': case '1': case '2': @@ -1402,85 +1603,109 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy194; + case '9': goto yy220; default: goto yy34; } -yy196: +yy222: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy219; + case 'c': goto yy252; default: goto yy34; } -yy197: +yy223: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy220; + case 'y': goto yy253; default: goto yy34; } -yy198: +yy224: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy221; + case 'e': goto yy254; default: goto yy34; } -yy199: +yy225: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy222; + case 'a': goto yy255; default: goto yy34; } -yy200: +yy226: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy223; + case 'a': goto yy256; default: goto yy34; } -yy201: +yy227: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy224; + case 'd': goto yy257; default: goto yy34; } -yy202: +yy228: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy225; + case 'e': goto yy258; default: goto yy34; } -yy203: +yy229: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy226; + case 'r': goto yy259; default: goto yy34; } -yy204: +yy230: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy227; + case 't': goto yy260; default: goto yy34; } -yy205: +yy231: yych = *++YYCURSOR; switch (yych) { - case 'p': goto yy228; + case 'p': goto yy261; default: goto yy34; } -yy206: +yy232: yych = *++YYCURSOR; switch (yych) { - case 'd': goto yy229; + case 'd': goto yy262; default: goto yy34; } -yy207: +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 'e': goto yy230; + case 'a': goto yy267; default: goto yy34; } -yy208: +yy238: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy231; + case 0x00: goto yy268; case '0': case '1': case '2': @@ -1490,26 +1715,32 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy208; + case '9': goto yy238; default: goto yy34; } -yy210: +yy240: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 3; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 5; -#line 122 "src/sfizz/OpcodeCleanup.re" +#line 131 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("fil", group(1), "_type"); goto end_region; } -#line 1509 "src/sfizz/OpcodeCleanup.cpp" -yy212: +#line 1734 "src/sfizz/OpcodeCleanup.cpp" +yy242: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy270; + default: goto yy137; + } +yy243: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy233; + case 0x00: goto yy271; case '0': case '1': case '2': @@ -1519,26 +1750,38 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy212; + case '9': goto yy243; default: goto yy34; } -yy214: +yy245: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy235; + case 'f': goto yy273; default: goto yy34; } -yy215: +yy246: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy216; + case 'e': goto yy274; default: goto yy34; } -yy216: +yy247: + yych = *++YYCURSOR; + switch (yych) { + case 'o': goto yy249; + default: goto yy34; + } +yy248: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy275; + default: goto yy34; + } +yy249: yych = *++YYCURSOR; - if (yych <= 0x00) goto yy236; + if (yych <= 0x00) goto yy276; goto yy34; -yy217: +yy250: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1547,13 +1790,13 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yypmatch[2] = yyt1 - 4; yypmatch[3] = yyt1 - 2; yypmatch[5] = YYCURSOR - 1; -#line 141 "src/sfizz/OpcodeCleanup.re" +#line 155 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("start_", group(1), "cc", group(2)); goto end_region; } -#line 1556 "src/sfizz/OpcodeCleanup.cpp" -yy219: +#line 1799 "src/sfizz/OpcodeCleanup.cpp" +yy252: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1567,81 +1810,111 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt1 = YYCURSOR; - goto yy238; + goto yy278; default: goto yy34; } -yy220: +yy253: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy240; + case '_': goto yy280; default: goto yy34; } -yy221: +yy254: yych = *++YYCURSOR; switch (yych) { case 0x00: yyt2 = yyt3 = NULL; - goto yy241; + goto yy281; case '_': yyt3 = YYCURSOR; - goto yy243; + goto yy283; default: goto yy34; } -yy222: +yy255: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy245; + case 'c': goto yy285; default: goto yy34; } -yy223: +yy256: yych = *++YYCURSOR; switch (yych) { - case 'y': goto yy224; + case 'y': goto yy257; default: goto yy34; } -yy224: +yy257: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy246; + case 'c': goto yy286; default: goto yy34; } -yy225: +yy258: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy247; + case 'a': goto yy287; default: goto yy34; } -yy226: +yy259: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy224; + case 't': goto yy257; default: goto yy34; } -yy227: +yy260: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy248; + case 'a': goto yy288; default: goto yy34; } -yy228: +yy261: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy249; + case 't': goto yy289; default: goto yy34; } -yy229: +yy262: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy250; + case 'e': goto yy290; default: goto yy34; } -yy230: +yy263: yych = *++YYCURSOR; switch (yych) { - case 'q': goto yy250; + case 'q': goto yy290; default: goto yy34; } -yy231: +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; @@ -1652,13 +1925,19 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 97 "src/sfizz/OpcodeCleanup.re" +#line 99 "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 1934 "src/sfizz/OpcodeCleanup.cpp" +yy270: + yych = *++YYCURSOR; + switch (yych) { + case 'm': goto yy298; + default: goto yy137; + } +yy271: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1667,19 +1946,31 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yypmatch[2] = yyt1 - 8; yypmatch[3] = yyt1 - 6; yypmatch[5] = YYCURSOR - 1; -#line 163 "src/sfizz/OpcodeCleanup.re" +#line 182 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "hdcc", group(2)); goto end_region; } -#line 1676 "src/sfizz/OpcodeCleanup.cpp" -yy235: +#line 1955 "src/sfizz/OpcodeCleanup.cpp" +yy273: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy216; + case 'f': goto yy299; default: goto yy34; } -yy236: +yy274: + yych = *++YYCURSOR; + switch (yych) { + case 't': goto yy249; + default: goto yy34; + } +yy275: + yych = *++YYCURSOR; + switch (yych) { + case 'a': goto yy300; + default: goto yy34; + } +yy276: ++YYCURSOR; yynmatch = 3; yypmatch[2] = yyt1; @@ -1688,16 +1979,16 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yypmatch[1] = YYCURSOR; yypmatch[3] = yyt2 - 1; yypmatch[5] = YYCURSOR - 1; -#line 101 "src/sfizz/OpcodeCleanup.re" +#line 103 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat(group(1), "_", group(2), "1"); goto end_region; } -#line 1697 "src/sfizz/OpcodeCleanup.cpp" -yy238: +#line 1988 "src/sfizz/OpcodeCleanup.cpp" +yy278: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy251; + case 0x00: goto yy301; case '0': case '1': case '2': @@ -1707,72 +1998,136 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy238; + case '9': goto yy278; default: goto yy34; } -yy240: +yy280: yych = *++YYCURSOR; switch (yych) { - case 'g': goto yy253; + case 'g': goto yy303; default: goto yy34; } -yy241: +yy281: ++YYCURSOR; yynmatch = 2; yypmatch[0] = yyt1; yypmatch[2] = yyt3; yypmatch[3] = yyt2; yypmatch[1] = YYCURSOR; -#line 158 "src/sfizz/OpcodeCleanup.re" +#line 177 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("resonance1", group(1)); goto end_region; } -#line 1732 "src/sfizz/OpcodeCleanup.cpp" -yy243: +#line 2023 "src/sfizz/OpcodeCleanup.cpp" +yy283: yych = *++YYCURSOR; if (yych <= 0x00) { yyt2 = YYCURSOR; - goto yy241; + goto yy281; } - goto yy243; -yy245: + goto yy283; +yy285: yych = *++YYCURSOR; switch (yych) { - case 'k': goto yy224; + case 'k': goto yy257; default: goto yy34; } -yy246: +yy286: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy254; + case 'c': goto yy304; default: goto yy34; } -yy247: +yy287: yych = *++YYCURSOR; switch (yych) { - case 's': goto yy255; + case 's': goto yy305; default: goto yy34; } -yy248: +yy288: yych = *++YYCURSOR; switch (yych) { - case 'i': goto yy256; + case 'i': goto yy306; default: goto yy34; } -yy249: +yy289: yych = *++YYCURSOR; switch (yych) { - case 'h': goto yy250; + case 'h': goto yy290; default: goto yy34; } -yy250: +yy290: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy257; + case 'c': goto yy307; default: goto yy34; } -yy251: +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; + yypmatch[4] = yyt2; + yypmatch[5] = yyt3; + yypmatch[6] = yyt5; + yypmatch[7] = yyt4; + yypmatch[0] = yyt1; + yypmatch[1] = YYCURSOR; + yypmatch[3] = yyt2 - 1; +#line 111 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); + goto end_region; + } +#line 2095 "src/sfizz/OpcodeCleanup.cpp" +yy295: + yych = *++YYCURSOR; + if (yych <= 0x00) { + yyt4 = YYCURSOR; + goto yy293; + } + goto yy295; +yy297: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy310; + default: goto yy34; + } +yy298: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy311; + goto yy136; +yy299: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: + yyt4 = yyt5 = NULL; + yyt3 = YYCURSOR; + goto yy313; + case '_': + yyt3 = yyt5 = YYCURSOR; + goto yy315; + default: goto yy34; + } +yy300: + yych = *++YYCURSOR; + switch (yych) { + case 'n': goto yy317; + default: goto yy34; + } +yy301: ++YYCURSOR; yynmatch = 3; yypmatch[4] = yyt1; @@ -1781,19 +2136,19 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yypmatch[2] = yyt1 - 6; yypmatch[3] = yyt1 - 4; yypmatch[5] = YYCURSOR - 1; -#line 145 "src/sfizz/OpcodeCleanup.re" +#line 159 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("start_", group(1), "hdcc", group(2)); goto end_region; } -#line 1790 "src/sfizz/OpcodeCleanup.cpp" -yy253: +#line 2145 "src/sfizz/OpcodeCleanup.cpp" +yy303: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy258; + case 'r': goto yy318; default: goto yy34; } -yy254: +yy304: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1807,37 +2162,96 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt3 = YYCURSOR; - goto yy259; + goto yy319; default: goto yy34; } -yy255: +yy305: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy224; + case 'e': goto yy257; default: goto yy34; } -yy256: +yy306: yych = *++YYCURSOR; switch (yych) { - case 'n': goto yy224; + case 'n': goto yy257; default: goto yy34; } -yy257: +yy307: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy261; + case 'c': goto yy321; default: goto yy34; } -yy258: +yy308: yych = *++YYCURSOR; switch (yych) { - case 'o': goto yy262; + case 'o': goto yy322; default: goto yy34; } -yy259: +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; + yypmatch[4] = yyt2; + yypmatch[5] = yyt3; + yypmatch[6] = yyt5; + yypmatch[7] = yyt4; + yypmatch[0] = yyt1; + yypmatch[1] = YYCURSOR; + yypmatch[3] = yyt2 - 1; +#line 107 "src/sfizz/OpcodeCleanup.re" + { + opcode = absl::StrCat(group(1), "_", group(2), "1", group(3)); + goto end_region; + } +#line 2232 "src/sfizz/OpcodeCleanup.cpp" +yy315: + yych = *++YYCURSOR; + if (yych <= 0x00) { + yyt4 = YYCURSOR; + goto yy313; + } + goto yy315; +yy317: + yych = *++YYCURSOR; + switch (yych) { + case 'c': goto yy324; + default: goto yy34; + } +yy318: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy263; + case 'o': goto yy325; + default: goto yy34; + } +yy319: + yych = *++YYCURSOR; + switch (yych) { + case 0x00: goto yy326; case '0': case '1': case '2': @@ -1847,10 +2261,10 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy259; + case '9': goto yy319; default: goto yy34; } -yy261: +yy321: yych = *++YYCURSOR; switch (yych) { case '0': @@ -1864,16 +2278,32 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt3 = YYCURSOR; - goto yy265; + goto yy328; default: goto yy34; } -yy262: +yy322: yych = *++YYCURSOR; switch (yych) { - case 'u': goto yy267; + case 'm': goto yy330; default: goto yy34; } -yy263: +yy323: + yych = *++YYCURSOR; + if (yych <= 0x00) goto yy331; + goto yy200; +yy324: + yych = *++YYCURSOR; + switch (yych) { + case 'e': goto yy299; + default: goto yy34; + } +yy325: + yych = *++YYCURSOR; + switch (yych) { + case 'u': goto yy333; + default: goto yy34; + } +yy326: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -1884,16 +2314,16 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 93 "src/sfizz/OpcodeCleanup.re" +#line 95 "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 2323 "src/sfizz/OpcodeCleanup.cpp" +yy328: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy268; + case 0x00: goto yy334; case '0': case '1': case '2': @@ -1903,16 +2333,32 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy265; + case '9': goto yy328; default: goto yy34; } -yy267: +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 yy270; + case 'p': goto yy336; default: goto yy34; } -yy268: +yy334: ++YYCURSOR; yynmatch = 4; yypmatch[2] = yyt1; @@ -1923,27 +2369,27 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc yypmatch[3] = yyt2 - 1; yypmatch[5] = yyt3 - 2; yypmatch[7] = YYCURSOR - 1; -#line 89 "src/sfizz/OpcodeCleanup.re" +#line 91 "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 2378 "src/sfizz/OpcodeCleanup.cpp" +yy336: yych = *++YYCURSOR; if (yych >= 0x01) goto yy34; ++YYCURSOR; yynmatch = 1; yypmatch[0] = YYCURSOR - 16; yypmatch[1] = YYCURSOR; -#line 127 "src/sfizz/OpcodeCleanup.re" +#line 136 "src/sfizz/OpcodeCleanup.re" { opcode = "group"; goto end_region; } -#line 1945 "src/sfizz/OpcodeCleanup.cpp" +#line 2391 "src/sfizz/OpcodeCleanup.cpp" } -#line 172 "src/sfizz/OpcodeCleanup.re" +#line 191 "src/sfizz/OpcodeCleanup.re" end_region: @@ -1958,80 +2404,80 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc YYCURSOR = opcode.c_str(); -#line 1962 "src/sfizz/OpcodeCleanup.cpp" +#line 2408 "src/sfizz/OpcodeCleanup.cpp" { char yych; yych = *YYCURSOR; switch (yych) { - case 's': goto yy277; - default: goto yy275; + case 's': goto yy343; + default: goto yy341; } -yy275: +yy341: ++YYCURSOR; -yy276: -#line 192 "src/sfizz/OpcodeCleanup.re" +yy342: +#line 211 "src/sfizz/OpcodeCleanup.re" { goto end_control; } -#line 1977 "src/sfizz/OpcodeCleanup.cpp" -yy277: +#line 2423 "src/sfizz/OpcodeCleanup.cpp" +yy343: yych = *(YYMARKER = ++YYCURSOR); switch (yych) { - case 'e': goto yy278; - default: goto yy276; + case 'e': goto yy344; + default: goto yy342; } -yy278: +yy344: yych = *++YYCURSOR; switch (yych) { - case 't': goto yy280; - default: goto yy279; + case 't': goto yy346; + default: goto yy345; } -yy279: +yy345: YYCURSOR = YYMARKER; - goto yy276; -yy280: + goto yy342; +yy346: yych = *++YYCURSOR; switch (yych) { - case '_': goto yy281; - default: goto yy279; + case '_': goto yy347; + default: goto yy345; } -yy281: +yy347: yych = *++YYCURSOR; switch (yych) { - case 'r': goto yy282; - default: goto yy279; + case 'r': goto yy348; + default: goto yy345; } -yy282: +yy348: yych = *++YYCURSOR; switch (yych) { - case 'e': goto yy283; - default: goto yy279; + case 'e': goto yy349; + default: goto yy345; } -yy283: +yy349: yych = *++YYCURSOR; switch (yych) { - case 'a': goto yy284; - default: goto yy279; + case 'a': goto yy350; + default: goto yy345; } -yy284: +yy350: yych = *++YYCURSOR; switch (yych) { - case 'l': goto yy285; - default: goto yy279; + case 'l': goto yy351; + default: goto yy345; } -yy285: +yy351: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy286; - default: goto yy279; + case 'c': goto yy352; + default: goto yy345; } -yy286: +yy352: yych = *++YYCURSOR; switch (yych) { - case 'c': goto yy287; - default: goto yy279; + case 'c': goto yy353; + default: goto yy345; } -yy287: +yy353: yych = *++YYCURSOR; switch (yych) { case '0': @@ -2045,13 +2491,13 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '8': case '9': yyt1 = YYCURSOR; - goto yy288; - default: goto yy279; + goto yy354; + default: goto yy345; } -yy288: +yy354: yych = *++YYCURSOR; switch (yych) { - case 0x00: goto yy290; + case 0x00: goto yy356; case '0': case '1': case '2': @@ -2061,24 +2507,24 @@ static std::string cleanUpOpcodeName(absl::string_view rawOpcode, OpcodeScope sc case '6': case '7': case '8': - case '9': goto yy288; - default: goto yy279; + case '9': goto yy354; + default: goto yy345; } -yy290: +yy356: ++YYCURSOR; yynmatch = 2; yypmatch[2] = yyt1; yypmatch[0] = yyt1 - 10; yypmatch[1] = YYCURSOR; yypmatch[3] = YYCURSOR - 1; -#line 187 "src/sfizz/OpcodeCleanup.re" +#line 206 "src/sfizz/OpcodeCleanup.re" { opcode = absl::StrCat("set_hdcc", group(1)); goto end_control; } -#line 2080 "src/sfizz/OpcodeCleanup.cpp" +#line 2526 "src/sfizz/OpcodeCleanup.cpp" } -#line 196 "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 3b0774e24..245f62d5e 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(); @@ -85,6 +86,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 +104,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; @@ -129,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; @@ -147,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/Oversampler.cpp b/src/sfizz/Oversampler.cpp index 1027b8bad..b4f216219 100644 --- a/src/sfizz/Oversampler.cpp +++ b/src/sfizz/Oversampler.cpp @@ -7,7 +7,12 @@ #include "Oversampler.h" #include "Buffer.h" #include "AudioSpan.h" +#include "AudioReader.h" #include "SIMDConfig.h" +#include + +template +using aligned_vector = std::vector>; constexpr std::array coeffsStage2x { 0.036681502163648017, @@ -69,28 +74,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); + aligned_vector upsampler2x; + aligned_vector upsampler4x; + aligned_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 +115,22 @@ 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; @@ -131,5 +139,101 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s if (framesReady != nullptr) framesReady->fetch_add(outputChunkSize); } +} + +void sfz::Oversampler::stream(AudioReader& input, AudioSpan output, std::atomic* framesReady) +{ + 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(); + + aligned_vector upsampler2x; + aligned_vector upsampler4x; + aligned_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; + outputFrameCounter += outputChunkSize; + if (framesReady != nullptr) + framesReady->fetch_add(outputChunkSize); + } } diff --git a/src/sfizz/Oversampler.h b/src/sfizz/Oversampler.h index 25e74eaf6..f6890ad28 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 @@ class Oversampler * @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; diff --git a/src/sfizz/Panning.cpp b/src/sfizz/Panning.cpp index 299f29551..f7b96bd9d 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 75d31ea4c..eddb4d480 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/PolyphonyGroup.cpp b/src/sfizz/PolyphonyGroup.cpp index 7ac1e3d64..d5b1615c4 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 d3e1413bf..7a4281af9 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -40,6 +40,10 @@ class PolyphonyGroup { * @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/PowerFollower.cpp b/src/sfizz/PowerFollower.cpp new file mode 100644 index 000000000..f2eb05e5b --- /dev/null +++ b/src/sfizz/PowerFollower.cpp @@ -0,0 +1,98 @@ +// 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; + } +} + +void PowerFollower::process(AudioSpan buffer) noexcept +{ + size_t numFrames = buffer.getNumFrames(); + if (numFrames == 0) + return; + + /// + constexpr size_t step = config::powerFollowerStep; + float currentPower = currentPower_; + float currentSum = currentSum_; + size_t currentCount = currentCount_; + + const float attackFactor = attackTrackingFactor_; + const float releaseFactor = releaseTrackingFactor_; + + /// + size_t index = 0; + while (index < numFrames) { + size_t blockSize = std::min(step - currentCount, numFrames - index); + absl::Span tempBuffer(tempBuffer_.get(), blockSize); + + 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); + + currentSum += sumSquares(tempBuffer); + currentCount += blockSize; + + if (currentCount == step) { + const float meanPower = currentSum / step; + currentPower = max( + currentPower * attackFactor + meanPower * (1 - attackFactor), + currentPower * releaseFactor + meanPower * (1 - releaseFactor)); + currentSum = 0; + currentCount = 0; + } + + index += blockSize; + } + + /// + currentPower_ = currentPower; + currentSum_ = currentSum; + currentCount_ = currentCount; +} + +void PowerFollower::clear() noexcept +{ + currentPower_ = 0; + currentSum_ = 0; + currentCount_ = 0; +} + +void PowerFollower::updateTrackingFactor() noexcept +{ + // Protect the envelope follower against blowups + attackTrackingFactor_ = std::exp(-1.0f / ((config::powerFollowerAttackTime / config::powerFollowerStep) * sampleRate_)); + releaseTrackingFactor_ = std::exp(-1.0f / ((config::powerFollowerReleaseTime / config::powerFollowerStep) * sampleRate_)); +} + +} // namespace sfz diff --git a/src/sfizz/PowerFollower.h b/src/sfizz/PowerFollower.h new file mode 100644 index 000000000..1febd9f12 --- /dev/null +++ b/src/sfizz/PowerFollower.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 "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 currentPower_; } + +private: + void updateTrackingFactor() noexcept; + +private: + float sampleRate_ {}; + unsigned samplesPerBlock_ {}; + + std::unique_ptr tempBuffer_; + + float attackTrackingFactor_ {}; + float releaseTrackingFactor_ {}; + + float currentPower_ {}; + float currentSum_ = 0; + size_t currentCount_ = 0; +}; + +} // namespace sfz diff --git a/src/sfizz/Range.h b/src/sfizz/Range.h index 3b5485358..7bbe369f0 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 @@ class Range { }; } + /** + * @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/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 1f24931c6..810b0d79f 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" @@ -44,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"): { @@ -111,7 +129,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 @@ -120,14 +138,21 @@ 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"): - 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)) - oscillator = *value; + 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); @@ -135,6 +160,16 @@ 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_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(); @@ -160,10 +195,17 @@ 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:" << std::string(opcode.value)); + DBG("Unkown off mode:" << opcode.value); } break; + case hash("off_time"): + offMode = SfzOffMode::time; + setValueFromOpcode(opcode, offTime, Default::egTimeRange); + break; case hash("polyphony"): if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) polyphony = *value; @@ -181,7 +223,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,13 +325,18 @@ 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; 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; @@ -311,7 +367,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"): @@ -332,7 +388,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& @@ -373,35 +429,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, 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, 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, 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, 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, ModKey::createNXYZ(ModId::Width, id)); break; case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); @@ -410,7 +466,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); @@ -463,7 +520,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"): @@ -475,7 +532,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&"): @@ -511,12 +568,33 @@ 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"): 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 @@ -535,30 +613,22 @@ 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)) return false; - setValueFromOpcode( - opcode, - filters[filterIndex].cutoffCC[opcode.parameters.back()], - Default::filterCutoffModRange - ); + 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)) return false; - setValueFromOpcode( - opcode, - filters[filterIndex].resonanceCC[opcode.parameters.back()], - Default::filterResonanceModRange - ); + processGenericCc(opcode, Default::filterResonanceModRange, ModKey::createNXYZ(ModId::FilResonance, id, filterIndex)); } break; case hash("fil&_keytrack"): // also fil_keytrack @@ -588,7 +658,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)) @@ -606,17 +676,13 @@ 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)) return false; - setValueFromOpcode( - opcode, - filters[filterIndex].gainCC[opcode.parameters.back()], - Default::filterGainModRange - ); + processGenericCc(opcode, Default::filterGainModRange, ModKey::createNXYZ(ModId::FilGain, id, filterIndex)); } break; case hash("fil&_type"): // also fil_type, filtype @@ -631,7 +697,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; @@ -639,115 +705,103 @@ 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& + case_any_ccN("eq&_bw"): // 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; - setValueFromOpcode(opcode, equalizers[eqNumber - 1].bandwidthCC[opcode.parameters.back()], Default::eqBandwidthModRange); + processGenericCc(opcode, Default::eqBandwidthModRange, ModKey::createNXYZ(ModId::EqBandwidth, id, eqIndex)); } break; case hash("eq&_freq"): { - 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].frequency, Default::eqFrequencyRange); + 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 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].frequencyCC[opcode.parameters.back()], Default::eqFrequencyModRange); + processGenericCc(opcode, Default::eqFrequencyModRange, ModKey::createNXYZ(ModId::EqFrequency, id, eqIndex)); } 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) - 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].gain, Default::eqGainRange); + 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 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].gainCC[opcode.parameters.back()], Default::eqGainModRange); + processGenericCc(opcode, Default::eqGainModRange, ModKey::createNXYZ(ModId::EqGain, id, eqIndex)); } 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; - DBG("Unknown EQ type: " << std::string(opcode.value)); + equalizers[eqIndex].type = EqType::kEqNone; + DBG("Unknown EQ type: " << opcode.value); } } break; // 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); @@ -765,7 +819,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, ModKey::createNXYZ(ModId::Pitch, id)); break; case hash("bend_up"): // also bendup setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); @@ -780,146 +834,697 @@ 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)) + lfos[lfoNumber - 1].phase0 = wrapPhase(*value); + } + 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)) { + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const 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)) { + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const 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)) { + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const 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)) { + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const 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)) { + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const 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)) { + const ModKey source = ModKey::createNXYZ(ModId::LFO, id, lfoNumber - 1); + const ModKey target = ModKey::createNXYZ(ModId::Volume, id); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + 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"): + { + 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; + 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; + + 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 ampeg = readBooleanFromOpcode(opcode)) { + FlexEGDescription& desc = flexEGs[egNumber - 1]; + 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"): - 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"): + case hash("ampeg_vel&decay"): + case hash("ampeg_vel&delay"): + case hash("ampeg_vel&hold"): + case hash("ampeg_vel&release"): + case hash("ampeg_vel&sustain"): + case hash("ampeg_attack_oncc&"): // also ampeg_attackcc& + case hash("ampeg_decay_oncc&"): // also ampeg_decaycc& + case hash("ampeg_delay_oncc&"): // also ampeg_delaycc& + case hash("ampeg_hold_oncc&"): // also ampeg_holdcc& + 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); + 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; + + 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; + + case hash("pitcheg_vel&depth"): if (opcode.parameters.front() != 2) return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2attack, Default::egOnCCTimeRange); + if (auto value = readOpcode(opcode.value, Default::pitchEgDepthRange)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::PitchEG, id), + ModKey::createNXYZ(ModId::Pitch, id)).velToDepth = *value; break; - case hash("ampeg_vel&decay"): + case hash("fileg_vel&depth"): if (opcode.parameters.front() != 2) return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2decay, Default::egOnCCTimeRange); + if (auto value = readOpcode(opcode.value, Default::filterEgDepthRange)) + getOrCreateConnection( + ModKey::createNXYZ(ModId::FilEG, id), + ModKey::createNXYZ(ModId::FilCutoff, id)).velToDepth = *value; break; - case hash("ampeg_vel&delay"): + + // 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(); + if (!effectNumber || effectNumber < 1 || effectNumber > config::maxEffectBuses) + break; + auto value = readOpcode(opcode.value, { 0, 100 }); + if (!value) + break; + if (static_cast(effectNumber + 1) > gainToEffect.size()) + gainToEffect.resize(effectNumber + 1); + gainToEffect[effectNumber] = *value / 100; + break; + } + + // Ignored opcodes + case hash("hichan"): + case hash("lochan"): + case hash("sw_default"): + case hash("ampeg_depth"): + case hash("ampeg_vel&depth"): + break; + default: + return false; + + #undef case_any_ccN + #undef LFO_EG_filter_EQ_target + } + + 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, amplitudeEG.vel2delay, Default::egOnCCTimeRange); + setValueFromOpcode(opcode, eg.vel2attack, Default::egOnCCTimeRange); break; - case hash("ampeg_vel&hold"): + case_any_eg("vel&decay"): if (opcode.parameters.front() != 2) return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2hold, Default::egOnCCTimeRange); + setValueFromOpcode(opcode, eg.vel2decay, Default::egOnCCTimeRange); break; - case hash("ampeg_vel&release"): + case_any_eg("vel&delay"): if (opcode.parameters.front() != 2) return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2release, Default::egOnCCTimeRange); + setValueFromOpcode(opcode, eg.vel2delay, Default::egOnCCTimeRange); break; - case hash("ampeg_vel&sustain"): + case_any_eg("vel&hold"): if (opcode.parameters.front() != 2) return false; // Was not vel2... - setValueFromOpcode(opcode, amplitudeEG.vel2sustain, Default::egOnCCPercentRange); + setValueFromOpcode(opcode, eg.vel2hold, Default::egOnCCTimeRange); break; - case hash("ampeg_attack_oncc&"): // also ampeg_attackcc& + 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)) - amplitudeEG.ccAttack[opcode.parameters.back()] = *value; + eg.ccAttack[opcode.parameters.back()] = *value; break; - case hash("ampeg_decay_oncc&"): // also ampeg_decaycc& + case_any_eg("decay_oncc&"): // also decaycc& if (opcode.parameters.back() >= config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccDecay[opcode.parameters.back()] = *value; + eg.ccDecay[opcode.parameters.back()] = *value; break; - case hash("ampeg_delay_oncc&"): // also ampeg_delaycc& + case_any_eg("delay_oncc&"): // also delaycc& if (opcode.parameters.back() >= config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccDelay[opcode.parameters.back()] = *value; + eg.ccDelay[opcode.parameters.back()] = *value; break; - case hash("ampeg_hold_oncc&"): // also ampeg_holdcc& + case_any_eg("hold_oncc&"): // also holdcc& if (opcode.parameters.back() >= config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccHold[opcode.parameters.back()] = *value; + eg.ccHold[opcode.parameters.back()] = *value; break; - case hash("ampeg_release_oncc&"): // also ampeg_releasecc& + case_any_eg("release_oncc&"): // also releasecc& if (opcode.parameters.back() >= config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) - amplitudeEG.ccRelease[opcode.parameters.back()] = *value; + eg.ccRelease[opcode.parameters.back()] = *value; break; - case hash("ampeg_start_oncc&"): // also ampeg_startcc& + case_any_eg("start_oncc&"): // also startcc& if (opcode.parameters.back() >= config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) - amplitudeEG.ccStart[opcode.parameters.back()] = *value; + eg.ccStart[opcode.parameters.back()] = *value; break; - case hash("ampeg_sustain_oncc&"): // also ampeg_sustaincc& + case_any_eg("sustain_oncc&"): // also sustaincc& if (opcode.parameters.back() >= config::numCCs) return false; if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) - amplitudeEG.ccSustain[opcode.parameters.back()] = *value; - - break; - - case hash("effect&"): - { - const auto effectNumber = opcode.parameters.back(); - if (!effectNumber || effectNumber < 1 || effectNumber > config::maxEffectBuses) - break; - auto value = readOpcode(opcode.value, { 0, 100 }); - if (!value) - break; - if (static_cast(effectNumber + 1) > gainToEffect.size()) - gainToEffect.resize(effectNumber + 1); - gainToEffect[effectNumber] = *value / 100; - break; - } + eg.ccSustain[opcode.parameters.back()] = *value; - // Ignored opcodes - case hash("hichan"): - case hash("lochan"): - case hash("sw_default"): - case hash("ampeg_depth"): - case hash("ampeg_vel&depth"): break; default: return false; - - #undef case_any_ccN } return true; + + #undef case_any_eg } -bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCMap *ccMap) +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()) return false; @@ -928,29 +1533,51 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, Range range, CCM if (ccNumber >= config::numCCs) return false; - if (ccMap) { - Modifier& modifier = (*ccMap)[ccNumber]; + 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.source.id() == ModId::Controller && + x.source.parameters().cc == ccNumber && + x.target == target; + }); + + Connection *conn; + if (it != connections.end()) + conn = &*it; + else { + connections.emplace_back(); + conn = &connections.back(); + conn->source = ModKey::createCC(ccNumber, 0, 0, 0); + conn->target = target; + } + + // + ModKey::Parameters p = conn->source.parameters(); switch (opcode.category) { case kOpcodeOnCcN: - setValueFromOpcode(opcode, modifier.value, range); + setValueFromOpcode(opcode, conn->sourceDepth, range); break; case kOpcodeCurveCcN: - setValueFromOpcode(opcode, modifier.curve, Default::curveCCRange); + 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, modifier.step, stepCCRange); + setValueFromOpcode(opcode, p.step, stepCCRange); } break; case kOpcodeSmoothCcN: - setValueFromOpcode(opcode, modifier.smooth, Default::smoothCCRange); + setValueFromOpcode(opcode, p.smooth, Default::smoothCCRange); break; default: assert(false); break; } - } + conn->source = ModKey(ModId::Controller, {}, p); + } return true; } @@ -965,12 +1592,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; @@ -982,18 +1605,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()) @@ -1026,24 +1642,37 @@ 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) < config::halfCCThreshold) - releaseTrigger = true; - else - noteIsOff = true; + if (midiState.getCCValue(sustainCC) < sustainThreshold) + 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 + + delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber)); } - return keyOk && velOk && randOk && releaseTrigger; + + return false; } bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept @@ -1057,11 +1686,6 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept if (!isSwitchedOn()) return false; - if (sustainCC == ccNumber && ccValue < config::halfCCThreshold && noteIsOff) { - noteIsOff = false; - return true; - } - if (!triggerOnCC) return false; @@ -1100,11 +1724,11 @@ 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 - pitchVariationInCents += static_cast(velocity) * pitchVeltrack; // track velocity + pitchVariationInCents += velocity * pitchVeltrack; // track velocity pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes return centsFactor(pitchVariationInCents); } @@ -1113,6 +1737,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; @@ -1120,16 +1747,21 @@ 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 { 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); } @@ -1142,7 +1774,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 @@ -1153,7 +1785,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 @@ -1213,19 +1848,14 @@ 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; + 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; } @@ -1296,3 +1926,32 @@ 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::getConnection(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); + 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; + c.target = target; + + 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 322f96ec5..8cf68067e 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -10,15 +10,18 @@ #include "LeakDetector.h" #include "Defaults.h" #include "EGDescription.h" +#include "FlexEGDescription.h" #include "EQDescription.h" #include "FilterDescription.h" +#include "LFODescription.h" #include "Opcode.h" #include "AudioBuffer.h" #include "MidiState.h" #include "FileId.h" -#include "NumericId.h" -#include "Modifiers.h" +#include "utility/NumericId.h" +#include "modulations/ModKey.h" #include "absl/types/optional.h" +#include "absl/strings/string_view.h" #include #include #include @@ -47,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; @@ -73,13 +79,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)? * @@ -235,16 +256,36 @@ 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 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. * * @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, const ModKey& target); void offsetAllKeys(int offset) noexcept; @@ -259,6 +300,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 @@ -273,21 +319,28 @@ 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 }; - 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 oscillatorMode = Default::oscillatorMode; int oscillatorMulti = Default::oscillatorMulti; float oscillatorDetune = Default::oscillatorDetune; + float oscillatorModDepth = Default::oscillatorModDepth; absl::optional oscillatorQuality; // Instrument settings: voice lifecycle 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 }; + bool rtDead { Default::rtDead }; // Region logic: key mapping Range keyRange { Default::keyRange }; //lokey, hikey and key @@ -306,6 +359,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 @@ -326,7 +380,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 @@ -341,14 +395,22 @@ 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; // 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 + float pitchRandom { Default::pitchRandom }; // pitch_random int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack int transpose { Default::transpose }; // transpose int tune { Default::tune }; // tune @@ -359,22 +421,40 @@ struct Region { // Envelopes EGDescription amplitudeEG; - EGDescription pitchEG; - EGDescription filterEG; + absl::optional pitchEG; + absl::optional filterEG; + + // Envelopes + std::vector flexEGs; + absl::optional flexAmpEG; // egN_ampeg + + // LFOs + std::vector lfos; bool hasStereoSample { false }; // Effects std::vector gainToEffect; - // Modifiers - ModifierArray> modifiers; - bool triggerOnCC { false }; // whether the region triggers on CC events or note events bool triggerOnNote { true }; - + + // Modulation matrix connections + struct Connection { + ModKey source; + ModKey target; + float sourceDepth = 0.0f; + float velToDepth = 0.0f; + }; + std::vector connections; + Connection* getConnection(const ModKey& source, const ModKey& target); + Connection& getOrCreateConnection(const ModKey& source, const ModKey& target); + // Parent RegionSet* parent { nullptr }; + + // Started notes + std::vector> delayedReleases; private: const MidiState& midiState; bool keySwitched { true }; @@ -383,7 +463,6 @@ struct Region { bool pitchSwitched { true }; bool bpmSwitched { true }; bool aftertouchSwitched { true }; - bool noteIsOff { false }; std::bitset ccSwitched; absl::string_view defaultPath { "" }; diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp index d6f8a5852..bfceb4e22 100644 --- a/src/sfizz/RegionSet.cpp +++ b/src/sfizz/RegionSet.cpp @@ -46,3 +46,15 @@ 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(); + }); +} + +void sfz::RegionSet::removeAllVoices() noexcept +{ + voices.clear(); +} diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 24120173a..4077fbe74 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,12 +81,23 @@ class RegionSet { * @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 * * @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 * @@ -103,8 +122,14 @@ class RegionSet { * @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 }; std::vector regions; std::vector subsets; std::vector voices; diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 0c28427e0..577ff7567 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -6,14 +6,14 @@ #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" #include "Tuning.h" +#include "modulations/ModMatrix.h" #include "absl/types/optional.h" namespace sfz @@ -28,23 +28,22 @@ struct Resources Logger logger; CurveSet curves; FilePool filePool { logger }; - FilterPool filterPool { midiState }; - EQPool eqPool { midiState }; 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 +53,7 @@ struct Resources wavePool.clearFileWaves(); logger.clear(); midiState.reset(); + modMatrix.clear(); } }; } diff --git a/src/sfizz/SIMDConfig.h b/src/sfizz/SIMDConfig.h index d06d66150..3f4c2ccc8 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 diff --git a/src/sfizz/SIMDHelpers.cpp b/src/sfizz/SIMDHelpers.cpp index c0e21d4e8..f34124a41 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; @@ -39,7 +41,9 @@ 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; private: std::array(SIMDOps::_sentinel)> simdStatus; @@ -79,11 +83,15 @@ 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) SIMD_OP(mean) - SIMD_OP(meanSquared) + SIMD_OP(sumSquares) + SIMD_OP(clampAll) + SIMD_OP(allWithin) } #undef SIMD_OP } @@ -114,11 +122,15 @@ 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) SIMD_OP(mean) - SIMD_OP(meanSquared) + SIMD_OP(sumSquares) + SIMD_OP(clampAll) + SIMD_OP(allWithin) } } #undef SIMD_OP @@ -152,13 +164,17 @@ 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); 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); } /// @@ -235,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 { @@ -284,9 +312,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 <> @@ -301,4 +329,16 @@ 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 +{ + 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 ec27824a1..a7b9a08ca 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 @@ -52,13 +53,17 @@ enum class SIMDOps { subtract1, multiplyAdd, multiplyAdd1, + multiplyMul, + multiplyMul1, copy, cumsum, diff, sfzInterpolationCast, mean, - meanSquared, + sumSquares, upsampling, + clampAll, + allWithin, _sentinel // }; @@ -174,13 +179,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()); } @@ -320,6 +325,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 * @@ -513,7 +568,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 @@ -521,13 +576,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 @@ -622,4 +697,58 @@ 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 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()); +} + +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) + * + * @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/ScopedFTZ.cpp b/src/sfizz/ScopedFTZ.cpp index 1f5b1c585..46ca9424b 100644 --- a/src/sfizz/ScopedFTZ.cpp +++ b/src/sfizz/ScopedFTZ.cpp @@ -18,19 +18,25 @@ ScopedFTZ::ScopedFTZ() #if SFIZZ_HAVE_SSE unsigned mask = _MM_DENORMALS_ZERO_MASK | _MM_FLUSH_ZERO_MASK; registerState = _mm_getcsr(); - _mm_setcsr((registerState & (~mask)) | mask); -#elif SFIZZ_HAVE_NEON - intptr_t mask = (1 << 24); + _mm_setcsr((static_cast(registerState) & (~mask)) | mask); +#elif SFIZZ_HAVE_NEON && SFIZZ_CPU_FAMILY_ARM + uintptr_t mask = 1u << 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 + uintptr_t mask = 1u << 24; + asm volatile("mrs %0, fpcr" : "=r"(registerState)); + asm volatile("msr fpcr, %0" : : "r"((registerState & (~mask)) | mask)); #endif } ScopedFTZ::~ScopedFTZ() { #if SFIZZ_HAVE_SSE - _mm_setcsr(registerState); -#elif SFIZZ_HAVE_NEON - asm volatile("vmrs %0, fpscr" : : "ri"(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 + asm volatile("mrs %0, fpcr" : : "r"(registerState)); #endif } diff --git a/src/sfizz/ScopedFTZ.h b/src/sfizz/ScopedFTZ.h index 6fea43b09..15f024d96 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 @@ class ScopedFTZ { ScopedFTZ(); ~ScopedFTZ(); private: - unsigned registerState; + uintptr_t registerState; }; diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index 31ea49c88..416b23041 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

& 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/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 789353353..bb6137bb3 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -56,6 +56,23 @@ struct SisterVoiceRing { return count; } + /** + * @brief Off all sisters in a ring + * + * @param voice + * @param delay + * @param fast whether to apply a fast release + */ + template>::value, int> = 0> + static void offAllSisters(T* voice, int delay, bool fast = false) { + if (voice != nullptr) { + SisterVoiceRing::applyToRing(voice, [&] (Voice* v) { + v->off(delay, fast); + }); + } + } + /** * @brief Check if a sister voice ring is well formed * @@ -112,14 +129,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 * @@ -129,6 +138,9 @@ class SisterVoiceRingBuilder { if (firstStartedVoice == nullptr) firstStartedVoice = voice; + firstStartedVoice->setPreviousSisterVoice(voice); + voice->setNextSisterVoice(firstStartedVoice); + if (lastStartedVoice != nullptr) { voice->setPreviousSisterVoice(lastStartedVoice); lastStartedVoice->setNextSisterVoice(voice); diff --git a/src/sfizz/Smoothers.cpp b/src/sfizz/Smoothers.cpp index 59fcc5b9d..fda61be0b 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); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c2c7779c1..0f11bba46 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -9,9 +9,18 @@ #include "Debug.h" #include "Macros.h" #include "MidiState.h" +#include "TriggerEvent.h" #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 "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" #include "absl/memory/memory.h" @@ -25,21 +34,29 @@ sfz::Synth::Synth() : Synth(config::numVoices) { - initializeSIMDDispatchers(); } sfz::Synth::Synth(int numVoices) { - const std::lock_guard disableCallback { callbackGuard }; + 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 resetVoices(numVoices); + + // modulation sources + genController.reset(new ControllerSource(resources)); + genLFO.reset(new LFOSource(*this)); + genFlexEnvelope.reset(new FlexEnvelopeSource(*this)); + genADSREnvelope.reset(new ADSREnvelopeSource(*this)); } sfz::Synth::~Synth() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -54,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); } @@ -61,20 +79,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); @@ -85,19 +102,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; @@ -129,11 +141,10 @@ 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); + // 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; }); @@ -151,6 +162,16 @@ 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::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); @@ -158,15 +179,20 @@ 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()); regions.push_back(std::move(lastRegion)); } void sfz::Synth::clear() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -175,10 +201,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); @@ -192,6 +216,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(); @@ -203,12 +230,23 @@ void sfz::Synth::clear() polyphonyGroups.emplace_back(); polyphonyGroups.back().setPolyphonyLimit(config::maxVoices); modificationTime = fs::file_time_type::min(); + + // set default controllers + 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) { for (auto& rawMember : members) { - const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal); + const Opcode member = rawMember.cleanUp(kOpcodeScopeMaster); switch (member.lettersOnlyHash) { case hash("polyphony"): @@ -216,6 +254,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; } } } @@ -257,6 +298,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; } }; @@ -286,24 +330,24 @@ 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&"): 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"): @@ -316,6 +360,38 @@ 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"): + 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: + DBG("Unsupported value for hint_stealing: " << member.value); + } + break; default: // Unsupported control opcode DBG("Unsupported control opcode: " << member.opcode); @@ -396,7 +472,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 +493,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; @@ -445,26 +521,38 @@ void sfz::Synth::finalizeSfzLoad() size_t maxFilters { 0 }; size_t maxEQs { 0 }; - ModifierArray maxModifiers { 0 }; + size_t maxLFOs { 0 }; + size_t maxFlexEGs { 0 }; + bool havePitchEG { false }; + bool haveFilterEG { false }; + + FlexEGs::clearUnusedCurves(); 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->loopBegin != Default::loopRange.getStart() && fileInformation->loopEnd != Default::loopRange.getEnd()) { + if (fileInformation->hasLoop) { if (region->loopRange.getStart() == Default::loopRange.getStart()) region->loopRange.setStart(fileInformation->loopBegin); @@ -475,12 +563,18 @@ 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); 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; @@ -491,12 +585,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; @@ -504,7 +594,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) { @@ -550,26 +640,66 @@ 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); + region->registerPitchWheel(0); region->registerAftertouch(0); 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()); + 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; } 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; + } + if (!usedCCs.test(11)) { + region.getOrCreateConnection( + ModKey::createCC(11, 4, defaultSmoothness, 0), + ModKey::createNXYZ(ModId::Amplitude, region.id)).sourceDepth = 100.0f; + } + } + modificationTime = checkModificationTime(); settingsPerVoice.maxFilters = maxFilters; settingsPerVoice.maxEQs = maxEQs; - settingsPerVoice.maxModifiers = maxModifiers; + settingsPerVoice.maxLFOs = maxLFOs; + settingsPerVoice.maxFlexEGs = maxFlexEGs; + settingsPerVoice.havePitchEG = havePitchEG; + settingsPerVoice.haveFilterEG = haveFilterEG; applySettingsPerVoice(); + + setupModMatrix(); } bool sfz::Synth::loadScalaFile(const fs::path& path) @@ -622,17 +752,22 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); + DBG("Engine hard polyphony reached"); return {}; } -int sfz::Synth::getNumActiveVoices() const noexcept +int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept { - auto activeVoices = 0; - for (const auto& voice : voices) { + if (!recompute) + return activeVoices; + + int active { 0 }; + for (auto& voice: voices) { if (!voice->isFree()) - activeVoices++; + active++; } - return activeVoices; + + return active; } void sfz::Synth::garbageCollect() noexcept @@ -641,9 +776,9 @@ 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 }; + const std::lock_guard disableCallback { callbackGuard }; this->samplesPerBlock = samplesPerBlock; for (auto& voice : voices) @@ -659,7 +794,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) @@ -673,18 +808,6 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept } } -void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept -{ - const Region* region = voice.getRegion(); - 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; @@ -698,7 +821,16 @@ 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 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; @@ -711,32 +843,47 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept return; } - int numActiveVoices { 0 }; + 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 }; - tempSpan->fill(0.0f); 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; - numActiveVoices++; - renderVoiceToOutputs(*voice, *tempSpan); + mm.beginVoice(voice->getId(), voice->getRegion()->getId(), voice->getTriggerEvent().value); + + activeVoices++; + + 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(); callbackBreakdown.panning += voice->getLastPanningDuration(); + mm.endVoice(); + if (voice->toBeCleanedUp()) voice->reset(); } @@ -764,25 +911,20 @@ 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()); } callbackBreakdown.dispatch = dispatchDuration; - resources.logger.logCallbackTime(callbackBreakdown, numActiveVoices, numFrames); + resources.logger.logCallbackTime(callbackBreakdown, activeVoices, numFrames); // 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))); @@ -797,7 +939,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 +955,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; @@ -828,136 +970,209 @@ 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); + checkEnginePolyphony(delay); + + Voice* selectedVoice = findFreeVoice(); + if (selectedVoice == nullptr) + return; + + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, triggerEvent); + ring.addVoiceToRing(selectedVoice); + engineSet->registerVoice(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); +} + +bool sfz::Synth::playingAttackVoice(const Region* releaseRegion) noexcept +{ + 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 { 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)) { - auto voice = findFreeVoice(); - if (voice == nullptr) + if (region->trigger == SfzTrigger::release && !region->rtDead && !playingAttackVoice(region)) continue; - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + startVoice(region, delay, triggerEvent, ring); } } } -void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept +void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept { - const auto randValue = randNoteDistribution(Random::randomGenerator); - SisterVoiceRingBuilder ring; + tempPolyphonyArray.clear(); + absl::c_copy_if(voiceViewArray, + std::back_inserter(tempPolyphonyArray), + [region](Voice* v) { return v->getRegion() == region && !v->releasedOrFree(); }); - for (auto& region : noteActivationLists[noteNumber]) { - 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 (tempPolyphonyArray.size() >= region->polyphony) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } +} - if (voice->getRegion() == region) { - regionPolyphonyArray.push_back(voice.get()); - } +void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept +{ + if (!region->notePolyphony) + return; - if (region->notePolyphony) { - if (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; - } + unsigned notePolyphonyCounter { 0 }; + Voice* selfMaskCandidate { nullptr }; + + for (Voice* voice : voiceViewArray) { + const sfz::TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent(); + 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) { + notePolyphonyCounter += 1; + switch (region->selfMask) { + case SfzSelfMask::mask: + if (voiceTriggerEvent.value <= triggerEvent.value) { + if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) { + selfMaskCandidate = voice; } } - - if (voice->checkOffGroup(delay, region->group)) - noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); + break; + case SfzSelfMask::dontMask: + if (!selfMaskCandidate || selfMaskCandidate->getAge() < voice->getAge()) + selfMaskCandidate = voice; + break; } + } + } - // 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 (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) { + SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); + } +} - auto parent = region->parent; +void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept +{ + const auto& activeVoices = polyphonyGroups[region->group].getActiveVoices(); + tempPolyphonyArray.clear(); + absl::c_copy_if(activeVoices, + std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); }); - // Polyphony reached on region - if (regionPolyphonyArray.size() >= region->polyphony) { - selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray)); - goto render; - } + if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } +} - // Polyphony reached on polyphony group - if (polyphonyGroups[region->group].getActiveVoices().size() - == polyphonyGroups[region->group].getPolyphonyLimit()) { - const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); - selectedVoice = stealer.steal(activeVoices); - goto render; - } +void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept +{ + auto parent = region->parent; + while (parent != nullptr) { + const auto& activeVoices = parent->getActiveVoices(); + tempPolyphonyArray.clear(); + absl::c_copy_if(activeVoices, + std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); }); - // Polyphony reached some parent group/master/etc - while (parent != nullptr) { - if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { - const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); - selectedVoice = stealer.steal(activeVoices); - goto render; - } - parent = parent->getParent(); - } + if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } - // Engine polyphony reached, we're stealing something - if (selectedVoice == nullptr) { - selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); - } + parent = parent->getParent(); + } +} - render: - // 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) { - renderVoiceToOutputs(*v, *tempSpan); - v->reset(); - }); +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); + 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(region, delay, noteNumber)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } } - // Voice should be free now - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); + startVoice(region, delay, triggerEvent, ring); } } } +void sfz::Synth::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +{ + if (!region->rtDead && !playingAttackVoice(region)) { + region->delayedReleases.clear(); + return; + } + + 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); 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); @@ -966,7 +1181,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; @@ -985,27 +1200,28 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept for (auto& voice : voices) voice->registerCC(delay, ccNumber, normValue); - SisterVoiceRingBuilder ring; + ccDispatch(delay, ccNumber, normValue); +} - for (auto& region : ccActivationLists[ccNumber]) { - if (region->registerCC(ccNumber, normValue)) { - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; +void sfz::Synth::initCc(int ccNumber, uint8_t ccValue) noexcept +{ + const float normValue = normalizeCC(ccValue); + initHdcc(ccNumber, normValue); +} - 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); - } +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); +} - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); - } - } +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 @@ -1033,6 +1249,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 { @@ -1110,13 +1349,27 @@ 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) { - 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()); + anonymousCCs.set(pair.first, false); + 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, 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"); + 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()); + } } } @@ -1135,25 +1388,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 @@ -1184,10 +1421,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; @@ -1201,10 +1438,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; @@ -1271,16 +1508,16 @@ void sfz::Synth::setVolume(float volume) noexcept int sfz::Synth::getNumVoices() const noexcept { - return numVoices; + return numRequiredVoices; } 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) + if (numVoices == this->numRequiredVoices) return; resetVoices(numVoices); @@ -1288,29 +1525,36 @@ 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); - for (int i = 0; i < numVoices; ++i) { + voiceViewArray.clear(); + voiceViewArray.reserve(numActualVoices); + + tempPolyphonyArray.clear(); + tempPolyphonyArray.reserve(numActualVoices); + + for (int i = 0; i < numActualVoices; ++i) { auto voice = absl::make_unique(i, resources); voice->setStateListener(this); + voiceViewArray.push_back(voice.get()); voices.emplace_back(std::move(voice)); } - voiceViewArray.clear(); - voiceViewArray.reserve(numVoices); - - regionPolyphonyArray.clear(); - regionPolyphonyArray.reserve(numVoices); - for (auto& voice : voices) { voice->setSampleRate(this->sampleRate); voice->setSamplesPerBlock(this->samplesPerBlock); - voiceViewArray.push_back(voice.get()); } - this->numVoices = numVoices; - applySettingsPerVoice(); } @@ -1319,13 +1563,93 @@ void sfz::Synth::applySettingsPerVoice() for (auto& voice : voices) { voice->setMaxFiltersPerVoice(settingsPerVoice.maxFilters); voice->setMaxEQsPerVoice(settingsPerVoice.maxEQs); - voice->prepareSmoothers(settingsPerVoice.maxModifiers); + voice->setMaxLFOsPerVoice(settingsPerVoice.maxLFOs); + voice->setMaxFlexEGsPerVoice(settingsPerVoice.maxFlexEGs); + 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() +{ + ModMatrix& mm = resources.modMatrix; + + for (const RegionPtr& region : regions) { + for (const Region::Connection& conn : region->connections) { + ModGenerator* gen = nullptr; + + 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; + case ModId::LFO: + gen = genLFO.get(); + break; + case ModId::Envelope: + gen = genFlexEnvelope.get(); + break; + case ModId::AmpEG: + case ModId::PitchEG: + case ModId::FilEG: + gen = genADSREnvelope.get(); + break; + default: + DBG("[sfizz] Have unknown type of source generator"); + break; + } + + ASSERT(gen); + if (!gen) + continue; + + ModMatrix::SourceId source = mm.registerSource(sourceKey, *gen); + ModMatrix::TargetId target = mm.registerTarget(targetKey); + + ASSERT(source); + if (!source) { + DBG("[sfizz] Failed to register modulation source"); + continue; + } + + ASSERT(target); + if (!target) { + DBG("[sfizz] Failed to register modulation target"); + continue; + } + + if (!mm.connect(source, target, conn.sourceDepth, conn.velToDepth)) { + 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 }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (factor == oversamplingFactor) @@ -1348,7 +1672,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 +1705,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 +1760,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(); @@ -1451,3 +1775,68 @@ 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); + 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); + 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 bcd795427..350e98bf0 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -17,15 +17,20 @@ #include "AudioSpan.h" #include "parser/Parser.h" #include "VoiceStealing.h" -#include "absl/types/span.h" +#include "utility/SpinMutex.h" +#include #include +#include #include -#include #include -#include #include 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 * of entry and in C the interface basically maps the functions of the class into @@ -210,6 +215,17 @@ class Synth final : public Voice::StateListener, public Parser::Listener { * @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. @@ -354,7 +370,30 @@ class Synth final : public Voice::StateListener, public Parser::Listener { * @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 + * + * @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; +public: /** + * @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 @@ -379,6 +418,29 @@ class Synth final : public Voice::StateListener, public Parser::Listener { * @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 @@ -396,7 +458,7 @@ class Synth final : public Voice::StateListener, public Parser::Listener { * * @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) * @@ -489,6 +551,7 @@ class Synth final : public Voice::StateListener, public Parser::Listener { */ void disableFreeWheeling() noexcept; + Resources& getResources() noexcept { return resources; } const Resources& getResources() const noexcept { return resources; } /** @@ -562,6 +625,13 @@ class Synth final : public Voice::StateListener, public Parser::Listener { */ 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. @@ -667,18 +737,53 @@ class Synth final : public Voice::StateListener, public Parser::Listener { void applySettingsPerVoice(); /** - * @brief Render the voice to its designated outputs and effect busses. - * - * @param voice - * @param tempSpan a temporary span used for rendering + * @brief Establish all connections of the modulation matrix. */ - void renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept; + void setupModMatrix(); + /** + * @brief Get the modification time of all included sfz files + * + * @return fs::file_time_type + */ 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 + 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; @@ -707,18 +812,92 @@ class Synth final : public Voice::StateListener, public Parser::Listener { 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 }; + 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; + // 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; - VoiceViewVector voiceViewArray; + /** + * @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 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. + * + * @param region + * @param delay + * @param triggerEvent + * @param ring + */ + void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) 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; + + /** + * @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; @@ -730,13 +909,15 @@ class Synth final : public Voice::StateListener, public Parser::Listener { 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 }; // 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; @@ -746,16 +927,30 @@ class Synth final : public Voice::StateListener, public Parser::Listener { int noteOffset { 0 }; int octaveOffset { 0 }; + // Modulation source generators + std::unique_ptr genController; + std::unique_ptr genLFO; + std::unique_ptr genFlexEnvelope; + std::unique_ptr genADSREnvelope; + // Settings per voice struct SettingsPerVoice { size_t maxFilters { 0 }; size_t maxEQs { 0 }; - ModifierArray maxModifiers { 0 }; + size_t maxLFOs { 0 }; + size_t maxFlexEGs { 0 }; + bool havePitchEG { false }; + bool haveFilterEG { false }; }; SettingsPerVoice settingsPerVoice; + // Controller initial values + std::array ccInitialValues; + Duration dispatchDuration { 0 }; + std::chrono::time_point lastGarbageCollection; + Parser parser; fs::file_time_type modificationTime { }; diff --git a/src/sfizz/TriggerEvent.h b/src/sfizz/TriggerEvent.h new file mode 100644 index 000000000..21108c021 --- /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 adfaa718e..2069d9b72 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -12,14 +12,22 @@ #include "SIMDHelpers.h" #include "Panning.h" #include "SfzHelpers.h" +#include "LFO.h" +#include "FlexEnvelope.h" +#include "modulations/ModId.h" +#include "modulations/ModKey.h" +#include "modulations/ModMatrix.h" #include "Interpolators.h" #include "absl/algorithm/container.h" 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); @@ -27,58 +35,56 @@ 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)); + // prepare curves + getSCurve(); } -void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept +sfz::Voice::~Voice() { - ASSERT(value >= 0.0f && value <= 1.0f); - - if (triggerType == TriggerType::CC) - number = region->pitchKeycenter; +} - this->triggerType = triggerType; - triggerNumber = number; - triggerValue = value; +void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noexcept +{ + 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); 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; - } - 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); + 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; + } } - 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) { @@ -89,81 +95,48 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, setupOscillatorUnison(); } else { currentPromise = resources.filePool.getFilePromise(region->sampleId); - if (currentPromise == nullptr) { + if (!currentPromise) { switchState(State::cleanMeUp); return; } - speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); + updateLoopInformation(); + 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 - 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(); - // 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, number, value); - if (newFilter) - filters.push_back(newFilter); + for (unsigned i = 0; i < region->filters.size(); ++i) { + filters[i].setup(*region, i, triggerEvent.number, triggerEvent.value); } - for (auto& eq: region->equalizers) { - auto newEQ = resources.eqPool.getEQ(eq, numChannels, value); - if (newEQ) - equalizers.push_back(newEQ); + for (unsigned i = 0; i < region->equalizers.size(); ++i) { + equalizers[i].setup(*region, i, triggerEvent.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); - - 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, region->getId(), delay); + saveModulationTargets(region); } int sfz::Voice::getCurrentSampleQuality() const noexcept @@ -177,16 +150,37 @@ 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; - if (egEnvelope.getRemainingDelay() > delay) { - switchState(State::cleanMeUp); - } else { - egEnvelope.startRelease(delay, fastRelease); + 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); +} + +void sfz::Voice::off(int delay, bool fast) noexcept +{ + if (!region->flexAmpEG) { + if (region->offMode == SfzOffMode::fast || fast) { + egAmplitude.setReleaseTime(Default::offTime); + } else if (region->offMode == SfzOffMode::time) { + egAmplitude.setReleaseTime(region->offTime); + } + } + else { + // TODO(jpc): Flex AmpEG + } + + release(delay); } void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept @@ -200,13 +194,13 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex if (state != State::playing) return; - if (triggerNumber == noteNumber) { + if (triggerEvent.number == noteNumber && triggerEvent.type == TriggerEventType::NoteOn) { noteIsOff = true; 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 +214,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); } @@ -252,17 +246,25 @@ 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); + + for (auto& filter : filters) + filter.setSampleRate(sampleRate); + + for (auto& eq : equalizers) + eq.setSampleRate(sampleRate); + + powerFollower.setSampleRate(sampleRate); } void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept { this->samplesPerBlock = samplesPerBlock; - this->minEnvelopeDelay = samplesPerBlock / 2; + powerFollower.setSamplesPerBlock(samplesPerBlock); } void sfz::Voice::renderBlock(AudioSpan buffer) noexcept @@ -270,7 +272,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); @@ -278,7 +281,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); @@ -294,10 +297,16 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept panStageMono(buffer); } - if (!egEnvelope.isSmoothing()) - switchState(State::cleanMeUp); + if (!region->flexAmpEG) { + if (!egAmplitude.isSmoothing()) + switchState(State::cleanMeUp); + } + else { + if (flexEGs[*region->flexAmpEG]->isFinished()) + switchState(State::cleanMeUp); + } - updateChannelPowers(buffer); + powerFollower.process(buffer); age += buffer.getNumFrames(); if (triggerDelay) { @@ -347,7 +356,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 +365,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); @@ -373,30 +382,26 @@ 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; - // AmpEG envelope - egEnvelope.getBlock(modulationSpan); + // Amplitude EG + absl::Span ampegOut(mm.getModulation(masterAmplitudeTarget), numSamples); + ASSERT(ampegOut.data()); + copy(ampegOut, 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.getModulation(amplitudeTarget)) { + 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.getModulation(volumeTarget)) { + for (size_t i = 0; i < numSamples; ++i) + modulationSpan[i] *= db2mag(mod[i]); + } // Smooth the gain transitions gainSmoother.process(modulationSpan, modulationSpan); @@ -441,20 +446,20 @@ 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; + // 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.getModulation(panTarget)) { + for (size_t i = 0; i < numSamples; ++i) + (*modulationSpan)[i] += normalizePercents(mod[i]); + } pan(*modulationSpan, leftBuffer, rightBuffer); } @@ -466,35 +471,37 @@ 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; + // 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.getModulation(panTarget)) { + 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.getModulation(widthTarget)) { + 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.getModulation(positionTarget)) { + for (size_t i = 0; i < numSamples; ++i) + (*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 @@ -504,12 +511,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); } } @@ -523,12 +530,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); } } @@ -538,53 +545,130 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept if (numSamples == 0) return; - if (currentPromise == nullptr) { + if (!currentPromise) { DBG("[Voice] Missing promise during fillWithData"); return; } 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); + + jumps->front() += floatPositionOffset; + cumsum(*jumps, *jumps); + sfzInterpolationCast(*jumps, *indices, *coeffs); + add1(sourcePosition, *indices); + } - fill(*jumps, pitchRatio * speedRatio); - pitchEnvelope(*jumps); - - 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 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 }); + // calculate loop characteristics + const auto loop = this->loop; + const bool isLooping = region->shouldLoop() + && (static_cast(loop.end) < source.getNumFrames()); + + /* + 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 <= loop.end) ? index : + (loop.start + (index - loop.start) % loop.size); + (*indices)[i] = wrappedIndex; + + // identify the partition this index is in + 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; + if (start) { + partitionStarts[numPartitions] = i; + partitionTypes[numPartitions] = partitionType; + ++numPartitions; } + + oldIndex = wrappedIndex; + oldPartitionType = partitionType; } - } else { + } + // 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(currentPromise->information.end), 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 - 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 - egEnvelope.startRelease(i, true); + 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; @@ -592,31 +676,121 @@ 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) { + 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) { + float pos = ptIndices[i] + ptCoeffs[i]; + xfCurvePos[i] = (pos - loop.xfOutStart) / loop.xfSize; + } + + //----------------------------------------------------------------// + // Crossfade Out + // -> fade out signal nearing the loop end + { + // compute out curve + absl::Span xfCurve = xfTemp2->first(ptSize); + 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 (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 (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); + } + // apply out curve + // (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 = xfIndicesTemp->first(ptSize); + absl::c_copy(ptIndices, xfInIndices.begin()); + subtract1(loop.xfOutStart - loop.xfInStart, 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 and coeffs + xfInIndices = xfInIndices.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); + + // compute in curve + absl::Span xfCurve = xfTemp2->first(applySize); + 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 (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 (config::loopXfadeCurve == 0) { + // TODO(jpc) vectorize this + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = clamp(xfInCurvePos[i], 0.0f, 1.0f); + } + // apply in curve + fillInterpolatedWithQuality( + source, xfInBuffer, xfInIndices, xfInCoeffs, xfCurve, quality); + } + } } 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))); @@ -624,31 +798,94 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept #endif } -template +template void sfz::Voice::fillInterpolated( - const sfz::AudioSpan& source, sfz::AudioSpan& dest, - absl::Span indices, absl::Span coeffs) + const sfz::AudioSpan& source, const sfz::AudioSpan& dest, + 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; + } +} + +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); @@ -680,24 +917,114 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept fill(*frequencies, pitchRatio * keycenterFrequency); pitchEnvelope(*frequencies); - if (waveUnisonSize == 1) { + auto detuneSpan = resources.bufferPool.getBuffer(numFrames); + if (!detuneSpan) + return; + + 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]; - osc.processModulated(frequencies->data(), 1.0, leftSpan.data(), buffer.getNumFrames()); - copy(leftSpan, rightSpan); + fill(*detuneSpan, 1.0f); + osc.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), buffer.getNumFrames()); + copy(*tempSpan, leftSpan); + copy(*tempSpan, rightSpan); } - else { - buffer.fill(0.0f); + else if (oscillatorMode <= 0 && oscillatorMulti >= 3) { + // unison oscillator + auto tempSpan = resources.bufferPool.getBuffer(numFrames); + auto tempLeftSpan = resources.bufferPool.getBuffer(numFrames); + auto tempRightSpan = resources.bufferPool.getBuffer(numFrames); + if (!tempSpan || !tempLeftSpan || !tempRightSpan) + return; + 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); + 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 auto tempSpan = resources.bufferPool.getBuffer(numFrames); 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); + 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 + // Note(jpc): not implemented, just do FM instead + goto fm_synthesis; + break; + + case 2: // FM synthesis + fm_synthesis: + fill(*detuneSpan, 1.0f); + multiplyAdd(*modulatorSpan, *frequencies, *frequencies); + oscCar.processModulated(frequencies->data(), detuneSpan->data(), tempSpan->data(), buffer.getNumFrames()); + break; } + + copy(*tempSpan, leftSpan); + copy(*tempSpan, rightSpan); } } @@ -709,16 +1036,15 @@ 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) - return false; - - if (delay <= this->triggerDelay) + if (region == nullptr || other == nullptr) return false; - if (triggerType == TriggerType::NoteOn && region->offBy == group) { - release(delay, region->offMode == SfzOffMode::fast); + if (triggerEvent.type == TriggerEventType::NoteOn + && region->offBy == other->group + && (region->group != other->group || noteNumber != triggerEvent.number)) { + off(delay); return true; } @@ -735,18 +1061,47 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; - for (auto& f : channelEnvelopeFilters) - f.reset(); + resetLoopInformation(); - for (auto& p : smoothedChannelEnvelopes) - p = 0.0f; + powerFollower.clear(); - filters.clear(); - equalizers.clear(); + for (auto& filter : filters) + filter.reset(); + + for (auto& eq : equalizers) + eq.reset(); 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& info = currentPromise->information; + const auto rate = info.sampleRate; + + 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 * 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 @@ -769,47 +1124,99 @@ 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]); + if (followPower) + return powerFollower.getAveragePower(); + else + return 0.0f; } bool sfz::Voice::releasedOrFree() const noexcept { - return state != State::playing || egEnvelope.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 +void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters) { - return sourcePosition; + if (numFilters == filters.size()) + return; + + filters.clear(); + for (unsigned i = 0; i < numFilters; ++i) + filters.emplace_back(resources); } -void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters) +void sfz::Voice::setMaxEQsPerVoice(size_t 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) { - // There are filters in there, this call is unexpected - ASSERT(filters.size() == 0); - filters.reserve(numFilters); + 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::setMaxEQsPerVoice(size_t numFilters) +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::setPitchEGEnabledPerVoice(bool havePitchEG) +{ + if (havePitchEG) + egPitch.reset(new ADSREnvelope); + else + egPitch.reset(); +} + +void sfz::Voice::setFilterEGEnabledPerVoice(bool haveFilterEG) { - // There are filters in there, this call is unexpected - ASSERT(equalizers.size() == 0); - equalizers.reserve(numFilters); + if (haveFilterEG) + egFilter.reset(new ADSREnvelope); + else + egFilter.reset(); } 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; } @@ -828,7 +1235,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; @@ -855,22 +1262,6 @@ void sfz::Voice::setupOscillatorUnison() #endif } -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 auto input = buffer.getConstSpan(i); - for (unsigned s = 0; s < buffer.getNumFrames(); ++s) - smoothedChannelEnvelopes[i] = - channelEnvelopeFilters[i].tickLowpass(std::abs(input[s])); - } -} - - void sfz::Voice::switchState(State s) { if (s != state) { @@ -880,12 +1271,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(); @@ -905,32 +1290,41 @@ 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; + + if (float* mod = mm.getModulation(pitchTarget)) { + for (size_t i = 0; i < numFrames; ++i) + pitchSpan[i] *= centsFactor(mod[i]); + } } 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); } + +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())); + 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())); + 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 74fa080cb..0503b6d7d 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -5,23 +5,29 @@ // 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" #include "Region.h" #include "AudioBuffer.h" #include "Resources.h" +#include "FilterPool.h" +#include "EQPool.h" #include "Smoothers.h" #include "AudioSpan.h" #include "LeakDetector.h" #include "OnePoleFilter.h" -#include "NumericId.h" +#include "PowerFollower.h" +#include "utility/NumericId.h" #include "absl/types/span.h" #include #include 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 @@ -38,11 +44,8 @@ class Voice { * @param midiState */ Voice(int voiceNumber, Resources& resources); - enum class TriggerType { - NoteOn, - NoteOff, - CC - }; + + ~Voice(); /** * @brief Get the unique identifier of this voice in a synth @@ -108,11 +111,9 @@ class Voice { * * @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. @@ -165,11 +166,12 @@ class Voice { * 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 @@ -193,23 +195,11 @@ class Voice { */ 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 @@ -257,19 +247,38 @@ class Voice { * * @return float */ - float getAverageEnvelope() const noexcept; + float getAveragePower() const noexcept; + /** - * @brief Get the position of the voice in the source, in samples + * @brief Enable the power follower * - * @return uint32_t */ - uint32_t getSourcePosition() const noexcept; + + 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! * * @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 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 * @@ -279,16 +288,49 @@ class Voice { /** * @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 Set the max number of Flex EGs per voice + * + * @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 * * @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 + * @param fast whether to apply a fast release regardless of the off mode + */ + void off(int delay, bool fast = false) noexcept; /** * @brief gets the age of the Voice @@ -302,7 +344,23 @@ class Voice { Duration getLastFilterDuration() const noexcept { return filterDuration; } Duration getLastPanningDuration() const noexcept { return panningDuration; } - void prepareSmoothers(const ModifierArray& numModifiers); + /** + * @brief Get the SFZv1 amplitude EG, if existing + */ + ADSREnvelope* getAmplitudeEG() { return &egAmplitude; } + /** + * @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: /** @@ -328,10 +386,32 @@ class Voice { * @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, AudioSpan& dest, - absl::Span indices, absl::Span coeffs); + const AudioSpan& source, const AudioSpan& dest, + 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 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 @@ -390,27 +470,6 @@ class Voice { */ 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. */ @@ -422,6 +481,12 @@ class Voice { */ 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; @@ -430,9 +495,7 @@ class Voice { State state { State::idle }; bool noteIsOff { false }; - TriggerType triggerType; - int triggerNumber; - float triggerValue; + TriggerEvent triggerEvent; absl::optional triggerDelay; float speedRatio { 1.0 }; @@ -445,19 +508,41 @@ class Voice { 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 }; + FileDataHolder currentPromise; int samplesPerBlock { config::defaultSamplesPerBlock }; - int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 }; float sampleRate { config::defaultSampleRate }; Resources& resources; - std::vector filters; - std::vector equalizers; + std::vector filters; + std::vector equalizers; + std::vector> lfos; + std::vector> flexEGs; - ADSREnvelope egEnvelope; + ADSREnvelope egAmplitude; + std::unique_ptr> egPitch; + std::unique_ptr> egFilter; float bendStepFactor { centsFactor(1) }; WavetableOscillator waveOscillators[config::oscillatorsPerVoice]; @@ -479,46 +564,63 @@ class Voice { fast_real_distribution uniformNoiseDist { -config::uniformNoiseBounds, config::uniformNoiseBounds }; fast_gaussian_generator gaussianNoiseDist { 0.0f, config::noiseVariance }; - ModifierArray> modifierSmoothers; Smoother gainSmoother; Smoother bendSmoother; Smoother xfadeSmoother; void resetSmoothers() noexcept; - std::array, 2> channelEnvelopeFilters; - std::array smoothedChannelEnvelopes; + ModMatrix::TargetId masterAmplitudeTarget; + ModMatrix::TargetId amplitudeTarget; + ModMatrix::TargetId volumeTarget; + ModMatrix::TargetId panTarget; + ModMatrix::TargetId positionTarget; + ModMatrix::TargetId widthTarget; + ModMatrix::TargetId pitchTarget; + ModMatrix::TargetId oscillatorDetuneTarget; + ModMatrix::TargetId oscillatorModDepthTarget; + + bool followPower { false }; + PowerFollower powerFollower; - HistoricalBuffer powerHistory { config::powerHistoryLength }; LEAK_DETECTOR(Voice); }; 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; -inline bool voiceOrdering(const Voice* lhs, const Voice* rhs) -{ - if (lhs->getAge() > rhs->getAge()) - return true; - if (lhs->getAge() < rhs->getAge()) + const TriggerEvent& lhsTrigger = lhs->getTriggerEvent(); + const TriggerEvent& rhsTrigger = rhs->getTriggerEvent(); + + if (lhsTrigger.number != rhsTrigger.number) return false; - if (lhs->getTriggerNumber() > rhs->getTriggerNumber()) - return true; - if (lhs->getTriggerNumber() < rhs->getTriggerNumber()) + if (lhsTrigger.value != rhsTrigger.value) return false; - if (lhs->getTriggerValue() > rhs->getTriggerValue()) - return true; - if (lhs->getTriggerValue() < rhs->getTriggerValue()) + if (lhsTrigger.type != rhsTrigger.type) return false; - if (lhs->getTriggerType() > rhs->getTriggerType()) - return true; + return true; +} + +inline bool voiceOrdering(const Voice* lhs, const Voice* rhs) +{ + if (lhs->getAge() != rhs->getAge()) + return lhs->getAge() > rhs->getAge(); + + const TriggerEvent& lhsTrigger = lhs->getTriggerEvent(); + const TriggerEvent& rhsTrigger = rhs->getTriggerEvent(); + + if (lhsTrigger.number != rhsTrigger.number) + return lhsTrigger.number < rhsTrigger.number; + + 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/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index b4eec3aaa..878f5fb8a 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -7,23 +7,53 @@ sfz::VoiceStealing::VoiceStealing() sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept { - // Start of the voice stealing algorithm + if (voices.empty()) + return {}; + + 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_sort(voices, voiceOrdering); + return voices.front(); +} + +sfz::Voice* sfz::VoiceStealing::stealEnvelopeAndAge(absl::Span voices) noexcept +{ absl::c_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 - / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + const auto powerThreshold = sumPower + / 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 // 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); Voice* returnedVoice = voices.front(); unsigned idx = 0; @@ -35,12 +65,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; } @@ -49,5 +79,6 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept do { idx++; } while (idx < voices.size() && sisterVoices(ref, voices[idx])); } + return returnedVoice; } diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h index 7251f37c4..47eb86925 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 @@ class VoiceStealing */ 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; diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index e294cfee8..a5655ce60 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,15 +60,14 @@ 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; } 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 +77,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; @@ -78,8 +85,7 @@ void WavetableOscillator::processModulatedSingle(const float* frequencies, float float frac = position - index; output[i] = interpolate(&table[index], frac); - phase += phaseInc; - phase -= static_cast(phase); + phase = incrementAndWrap(phase, phaseInc); } _phase = phase; @@ -103,15 +109,14 @@ 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; } 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 +126,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); @@ -132,8 +137,7 @@ void WavetableOscillator::processModulatedDual(const float* frequencies, float d (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; @@ -159,22 +163,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; } } @@ -283,65 +287,75 @@ 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) -{ - int oct = fp_exponent(frequencyScaleFactor * f); - return clamp(oct, 0, countOctaves - 1); -} +const float MipmapRange::K = 1.0 / F1; +const float MipmapRange::LogB = std::log(FN / F1) / (N - 1); -static const auto octaveForFrequencyTable = []() +const std::array MipmapRange::FrequencyToIndex = []() { - static constexpr unsigned N = 1024; - std::array table; - - constexpr double fmin = 1 / WavetableRange::frequencyScaleFactor; - constexpr double fmax = (1 << (WavetableRange::countOctaves - 1)) / WavetableRange::frequencyScaleFactor; + std::array table; - 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) { + 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; 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) +float MipmapRange::getExactIndexForFrequency(float f) { - WavetableRange range; + float t = (f < F1) ? 0.0f : (std::log(K * f) / LogB); + return clamp(t, 0, N - 1); +} - Fraction mant = fp_mantissa(0.0f); - float k = 1.0f / frequencyScaleFactor; +const std::array MipmapRange::IndexToStartFrequency = []() +{ + 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; + + return table; +}(); + +MipmapRange MipmapRange::getRangeForIndex(int o) +{ + o = clamp(o, 0, N - 1); - range.minFrequency = k * fp_from_parts(0, o, 0); - range.maxFrequency = k * fp_from_parts(0, o, mant.den - 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 +370,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; @@ -511,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); diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index e2f26cfbd..a7d907739 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 @@ class WavetableOscillator { */ 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. */ @@ -65,20 +71,20 @@ class WavetableOscillator { /** 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; @@ -117,36 +123,42 @@ class HarmonicProfile { }; /** - 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; - - static unsigned getOctaveForFrequency(float f); - static float getFractionalOctaveForFrequency(float f); - static WavetableRange getRangeForOctave(int o); - static WavetableRange 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 + // 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 float getIndexForFrequency(float f); + static float getExactIndexForFrequency(float f); + static MipmapRange getRangeForIndex(int o); + static MipmapRange getRangeForFrequency(float f); + + // 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 +171,7 @@ class WavetableMulti { 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 +182,7 @@ class WavetableMulti { // 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 +198,15 @@ class WavetableMulti { 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); } @@ -202,7 +214,9 @@ class WavetableMulti { // 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(); diff --git a/src/sfizz/effects/Apan.cpp b/src/sfizz/effects/Apan.cpp index c21af6c3b..a51f4a4ea 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/src/sfizz/effects/Compressor.cpp b/src/sfizz/effects/Compressor.cpp new file mode 100644 index 000000000..3aaf04a37 --- /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 000000000..ed2b0e5d6 --- /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/Disto.cpp b/src/sfizz/effects/Disto.cpp new file mode 100644 index 000000000..9e83856a8 --- /dev/null +++ b/src/sfizz/effects/Disto.cpp @@ -0,0 +1,231 @@ +// 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 "MathHelpers.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 + { + 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; } \ + 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 000000000..d9cc1ac70 --- /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/Fverb.cpp b/src/sfizz/effects/Fverb.cpp new file mode 100644 index 000000000..71e73efa1 --- /dev/null +++ b/src/sfizz/effects/Fverb.cpp @@ -0,0 +1,258 @@ +// 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 const Profile chamber; + + 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 + }; + 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() + : 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; + else if (value == "chamber") + profile = &Impl::chamber; + } + 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, 10.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 000000000..718f269eb --- /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/Gate.cpp b/src/sfizz/effects/Gate.cpp new file mode 100644 index 000000000..b7a819d50 --- /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%=gate open, 100%=gate closed) + + 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 000000000..bfe3ccfd1 --- /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/Width.cpp b/src/sfizz/effects/Width.cpp index 52d2876b3..a9c3c202a 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/effects/dsp/compressor.dsp b/src/sfizz/effects/dsp/compressor.dsp new file mode 100644 index 000000000..6675e7c1f --- /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/dsp/disto_stage.dsp b/src/sfizz/effects/dsp/disto_stage.dsp new file mode 100644 index 000000000..f05a54fa7 --- /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/dsp/fverb.dsp b/src/sfizz/effects/dsp/fverb.dsp new file mode 100644 index 000000000..30996e647 --- /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/dsp/gate.dsp b/src/sfizz/effects/dsp/gate.dsp new file mode 100644 index 000000000..7bac7f27d --- /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/compressor.cxx b/src/sfizz/effects/gen/compressor.cxx new file mode 100644 index 000000000..ba19b8702 --- /dev/null +++ b/src/sfizz/effects/gen/compressor.cxx @@ -0,0 +1,181 @@ +/* ------------------------------------------------------------ +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)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 diff --git a/src/sfizz/effects/gen/disto_stage.cxx b/src/sfizz/effects/gen/disto_stage.cxx new file mode 100644 index 000000000..e630bd682 --- /dev/null +++ b/src/sfizz/effects/gen/disto_stage.cxx @@ -0,0 +1,249 @@ +/* ------------------------------------------------------------ +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) { + (void)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 diff --git a/src/sfizz/effects/gen/fverb.cxx b/src/sfizz/effects/gen/fverb.cxx new file mode 100644 index 000000000..849c24854 --- /dev/null +++ b/src/sfizz/effects/gen/fverb.cxx @@ -0,0 +1,714 @@ +/* ------------------------------------------------------------ +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) { + (void)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 diff --git a/src/sfizz/effects/gen/gate.cxx b/src/sfizz/effects/gen/gate.cxx new file mode 100644 index 000000000..93da987b0 --- /dev/null +++ b/src/sfizz/effects/gen/gate.cxx @@ -0,0 +1,196 @@ +/* ------------------------------------------------------------ +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)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 diff --git a/src/sfizz/modulations/ModGenerator.h b/src/sfizz/modulations/ModGenerator.h new file mode 100644 index 000000000..7bb6c3291 --- /dev/null +++ b/src/sfizz/modulations/ModGenerator.h @@ -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 + +#pragma once +#include "utility/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) { (void)sampleRate; } + + /** + * @brief Set the maximum block size + */ + virtual void setSamplesPerBlock(unsigned count) { (void)count; } + + /** + * @brief Initialize the generator. + * + * @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, 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 + * + * @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; + + /** + * @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/ModId.cpp b/src/sfizz/modulations/ModId.cpp new file mode 100644 index 000000000..3d837ec59 --- /dev/null +++ b/src/sfizz/modulations/ModId.cpp @@ -0,0 +1,78 @@ +// 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; + case ModId::AmpEG: + return kModIsPerVoice; + case ModId::PitchEG: + return kModIsPerVoice; + case ModId::FilEG: + return kModIsPerVoice; + + // targets + case ModId::MasterAmplitude: + return kModIsPerVoice|kModIsPercentMultiplicative; + 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; + case ModId::FilGain: + return kModIsPerVoice|kModIsAdditive; + case ModId::FilCutoff: + return kModIsPerVoice|kModIsAdditive; + case ModId::FilResonance: + return kModIsPerVoice|kModIsAdditive; + case ModId::EqGain: + return kModIsPerVoice|kModIsAdditive; + case ModId::EqFrequency: + return kModIsPerVoice|kModIsAdditive; + case ModId::EqBandwidth: + return kModIsPerVoice|kModIsAdditive; + case ModId::OscillatorDetune: + return kModIsPerVoice|kModIsAdditive; + case ModId::OscillatorModDepth: + return kModIsPerVoice|kModIsPercentMultiplicative; + + // 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 000000000..48d9b2d96 --- /dev/null +++ b/src/sfizz/modulations/ModId.h @@ -0,0 +1,99 @@ +// 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, + AmpEG, + PitchEG, + FilEG, + + _SourcesEnd, + + //-------------------------------------------------------------------------- + // Targets + //-------------------------------------------------------------------------- + _TargetsStart = _SourcesEnd, + + MasterAmplitude = _TargetsStart, + Amplitude, + Pan, + Width, + Position, + Pitch, + Volume, + FilGain, + FilCutoff, + FilResonance, + EqGain, + EqFrequency, + EqBandwidth, + OscillatorDetune, + OscillatorModDepth, + + _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 000000000..051a4f8ef --- /dev/null +++ b/src/sfizz/modulations/ModKey.cpp @@ -0,0 +1,128 @@ +// 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 "../Debug.h" +#include + +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( + static_cast(this), + 0, sizeof(RawParameters)); +} + +ModKey::Parameters::Parameters(const Parameters& other) noexcept +{ + std::memcpy( + static_cast(this), + static_cast(&other), + sizeof(RawParameters)); +} + +ModKey::Parameters& ModKey::Parameters::operator=(const Parameters& other) noexcept +{ + if (this != &other) + std::memcpy( + static_cast(this), + static_cast(&other), + sizeof(RawParameters)); + return *this; +} + +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.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_); +} + + +std::string ModKey::toString() const +{ + switch (id_) { + case ModId::Controller: + return absl::StrCat("Controller ", params_.cc, + " {curve=", params_.curve, ", smooth=", params_.smooth, + ", step=", params_.step, "}"); + case ModId::Envelope: + 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: + 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: + return absl::StrCat("Pan {", region_.number(), "}"); + case ModId::Width: + return absl::StrCat("Width {", region_.number(), "}"); + case ModId::Position: + return absl::StrCat("Position {", region_.number(), "}"); + case ModId::Pitch: + 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=", 1 + params_.N, "}"); + case ModId::FilCutoff: + return absl::StrCat("FilterCutoff {", region_.number(), ", N=", 1 + params_.N, "}"); + case ModId::FilResonance: + return absl::StrCat("FilterResonance {", region_.number(), ", N=", 1 + params_.N, "}"); + case ModId::EqGain: + return absl::StrCat("EqGain {", region_.number(), ", N=", 1 + params_.N, "}"); + case ModId::EqFrequency: + 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, "}"); + case ModId::OscillatorModDepth: + return absl::StrCat("OscillatorModDepth {", region_.number(), ", N=", 1 + params_.N, "}"); + + default: + return {}; + } +} + +} // namespace sfz + diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h new file mode 100644 index 000000000..ce16c7a20 --- /dev/null +++ b/src/sfizz/modulations/ModKey.h @@ -0,0 +1,103 @@ +// 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 "ModId.h" +#include "../utility/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), flags_(ModIds::flags(id_)) {} + + 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(); } + + 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; + std::string toString() const; + + struct RawParameters { + union { + //! Parameters if this key identifies a CC source + 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; }; + // !!! NOTE: NXYZ is expected to be stored in 0-indexed form + // eg. `lfo1_eq2` is N=0, X=1 + }; + }; + + 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 + { + return id_ == other.id_ && region_ == other.region_ && + parameters() == other.parameters(); + } + + bool operator!=(const ModKey &other) const noexcept + { + return !this->operator==(other); + } + + +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_ {}; + // Memorize the flag + int flags_; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/ModKeyHash.cpp b/src/sfizz/modulations/ModKeyHash.cpp new file mode 100644 index 000000000..beeb90d33 --- /dev/null +++ b/src/sfizz/modulations/ModKeyHash.cpp @@ -0,0 +1,33 @@ +// 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.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 000000000..1f3ecbdcc --- /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 +#include + +namespace sfz { class ModKey; } + +namespace std { + 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 000000000..ffba3f6a7 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -0,0 +1,532 @@ +// 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 +#include + +namespace sfz { + +struct ModMatrix::Impl { + double sampleRate_ {}; + uint32_t samplesPerBlock_ {}; + + uint32_t numFrames_ {}; + NumericId currentVoiceId_ {}; + NumericId currentRegionId_ {}; + + float currentVoiceTriggerValue_ {}; + + struct Source { + ModKey key; + ModGenerator* gen {}; + bool bufferReady {}; + Buffer buffer; + }; + + struct ConnectionData { + float sourceDepth_ {}; + float velToDepth_ {}; + }; + + 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 sourceIndicesForGlobal_; + std::vector targetIndicesForGlobal_; + + int maxRegionIdx_ { -1 }; + std::vector> sourceIndicesForRegion_; + std::vector> targetIndicesForRegion_; + + std::vector sources_; + std::vector targets_; +}; + +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(); + impl.sourceIndicesForGlobal_.clear(); + impl.targetIndicesForGlobal_.clear(); + impl.sourceIndicesForRegion_.clear(); + impl.targetIndicesForRegion_.clear(); + impl.maxRegionIdx_ = -1; +} + +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); +} + +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(); + if (key.region().number() > impl.maxRegionIdx_) + impl.maxRegionIdx_ = key.region().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(); + if (key.region().number() > impl.maxRegionIdx_) + impl.maxRegionIdx_ = key.region().number(); + + return id; +} + +ModMatrix::SourceId ModMatrix::findSource(const ModKey& key) const +{ + 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) const +{ + 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, float sourceDepth, float velToDepth) +{ + 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]; + conn.sourceDepth_ = sourceDepth; + conn.velToDepth_ = velToDepth; + + return true; +} + +void ModMatrix::init() +{ + Impl& impl = *impl_; + + if (impl.maxRegionIdx_ >= 0) { + const size_t numRegions = impl.maxRegionIdx_ + 1; + impl.sourceIndicesForRegion_.resize(numRegions); + impl.targetIndicesForRegion_.resize(numRegions); + } + + for (unsigned i = 0; i < impl.sources_.size(); ++i) { + Impl::Source& source = impl.sources_[i]; + const int flags = source.key.flags(); + 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()); + impl.sourceIndicesForRegion_[source.key.region().number()].push_back(i); + } + } + + for (unsigned i = 0; i < impl.targets_.size(); ++i) { + Impl::Target& target = impl.targets_[i]; + const int flags = target.key.flags(); + 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); + } + } +} + +void ModMatrix::initVoice(NumericId voiceId, NumericId regionId, unsigned delay) +{ + Impl& impl = *impl_; + ASSERT(regionId); + ASSERT(static_cast(regionId.number()) < impl.sourceIndicesForRegion_.size()); + + const auto idNumber = static_cast(regionId.number()); + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { + const Impl::Source& source = impl.sources_[idx]; + source.gen->init(source.key, voiceId, delay); + } +} + +void ModMatrix::releaseVoice(NumericId voiceId, NumericId regionId, unsigned delay) +{ + Impl& impl = *impl_; + + ASSERT(regionId); + + const auto idNumber = static_cast(regionId.number()); + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { + const Impl::Source& source = impl.sources_[idx]; + source.gen->release(source.key, voiceId, delay); + } +} + +void ModMatrix::beginCycle(unsigned numFrames) +{ + Impl& impl = *impl_; + + impl.numFrames_ = numFrames; + + for (auto idx: impl.sourceIndicesForGlobal_) { + Impl::Source& source = impl.sources_[idx]; + source.bufferReady = false; + } + for (auto idx: impl.targetIndicesForGlobal_) { + Impl::Target& target = impl.targets_[idx]; + target.bufferReady = false; + } +} + +void ModMatrix::endCycle() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + + for (auto idx: impl.sourceIndicesForGlobal_) { + Impl::Source& source = impl.sources_[idx]; + if (!source.bufferReady) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, {}, buffer); + } + } + + impl.numFrames_ = 0; +} + +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()); + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { + Impl::Source& source = impl.sources_[idx]; + source.bufferReady = false; + } + + for (auto idx: impl.targetIndicesForRegion_[idNumber]) { + Impl::Target& target = impl.targets_[idx]; + target.bufferReady = false; + } +} + +void ModMatrix::endVoice() +{ + Impl& impl = *impl_; + const uint32_t numFrames = impl.numFrames_; + const NumericId voiceId = impl.currentVoiceId_; + const NumericId regionId = impl.currentRegionId_; + + ASSERT(regionId); + ASSERT(static_cast(regionId.number()) < impl.sourceIndicesForRegion_.size()); + + const auto idNumber = static_cast(regionId.number()); + + for (auto idx: impl.sourceIndicesForRegion_[idNumber]) { + const Impl::Source& source = impl.sources_[idx]; + if (!source.bufferReady) { + absl::Span buffer(source.buffer.data(), numFrames); + source.gen->generateDiscarded(source.key, voiceId, buffer); + } + } + + impl.currentVoiceId_ = {}; + impl.currentRegionId_ = {}; + + impl.currentVoiceTriggerValue_ = 0.0f; +} + +float* ModMatrix::getModulation(TargetId targetId) +{ + if (!validTarget(targetId)) + return nullptr; + + 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(); + + 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(); + + // set the ready flag to prevent a cycle + // in case there is, be sure to initialize the buffer + target.bufferReady = true; + + auto sourcesPos = target.connectedSources.begin(); + auto sourcesEnd = target.connectedSources.end(); + bool isFirstSource = true; + + // generate sources in their dedicated buffers + // then add or multiply, depending on target flags + while (sourcesPos != sourcesEnd) { + Impl::Source &source = impl.sources_[sourcesPos->first]; + 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) { + 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; + } + + 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) + buffer[i] = sourceDepth * sourceBuffer[i]; + } + else { + copy(absl::Span(sourceBuffer), buffer); + } + isFirstSource = false; + } + else { + if (targetFlags & kModIsMultiplicative) { + multiplyMul1(sourceDepth, sourceBuffer, buffer); + } + else if (targetFlags & kModIsPercentMultiplicative) { + multiplyMul1(0.01f * sourceDepth, sourceBuffer, buffer); + } + else { + ASSERT(targetFlags & kModIsAdditive); + multiplyAdd1(sourceDepth, sourceBuffer, buffer); + } + } + } + + ++sourcesPos; + } + + // if there were no source, fill output with the neutral element + if (isFirstSource) { + if (targetFlags & kModIsMultiplicative) + fill(buffer, 1.0f); + else if (targetFlags & kModIsPercentMultiplicative) + fill(buffer, 100.0f); + else { + ASSERT(targetFlags & kModIsAdditive); + fill(buffer, 0.0f); + } + } + + 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(); +} + +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; +} + +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 new file mode 100644 index 000000000..0a01dd5b0 --- /dev/null +++ b/src/sfizz/modulations/ModMatrix.h @@ -0,0 +1,217 @@ +// 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/NumericId.h" +#include +#include +#include + +namespace sfz { + +class ModKey; +class ModGenerator; +class Voice; +struct Region; + +/** + * @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) const; + + /** + * @brief Look up a target by key. + * + * @param key target key + */ + TargetId findTarget(const ModKey& key) const; + + /** + * @brief Connect a source and a destination inside the matrix. + * + * @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, float velToDepth = 0.0f); + + /** + * @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, 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. + * + * @param numFrames + */ + 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. + * + * @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, float triggerValue); + + /** + * @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. + * + * @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; + + /** + * @brief Get a representation of the matrix written as a Dot graph. + */ + 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_; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/ADSREnvelope.cpp b/src/sfizz/modulations/sources/ADSREnvelope.cpp new file mode 100644 index 000000000..57f8ba7da --- /dev/null +++ b/src/sfizz/modulations/sources/ADSREnvelope.cpp @@ -0,0 +1,130 @@ +// 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::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + desc = ®ion->amplitudeEG; + break; + 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::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + break; + 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::AmpEG: + eg = voice->getAmplitudeEG(); + ASSERT(eg); + break; + 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 000000000..b2644df9d --- /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 diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp new file mode 100644 index 000000000..c81229f24 --- /dev/null +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -0,0 +1,95 @@ +// 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, unsigned delay) +{ + (void)voiceId; + (void)delay; + + 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); + }; + + 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; + bool canShortcut = events.size() == 1; + 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 000000000..d5320cf3a --- /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, unsigned delay) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/FlexEnvelope.cpp b/src/sfizz/modulations/sources/FlexEnvelope.cpp new file mode 100644 index 000000000..557dd4dbb --- /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 000000000..c4e50fc46 --- /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 diff --git a/src/sfizz/modulations/sources/LFO.cpp b/src/sfizz/modulations/sources/LFO.cpp new file mode 100644 index 000000000..2f06c34f9 --- /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, unsigned delay) +{ + 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(delay); +} + +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 000000000..cf7e702f7 --- /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, unsigned delay) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + Synth* synth_ = nullptr; +}; + +} // namespace sfz diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index 071bc1e21..9ed460c2c 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/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index fbdd041cf..13cfd8a95 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 de33d8159..3e103b2eb 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/src/sfizz/simd/Common.h b/src/sfizz/simd/Common.h index 6fdddeabd..493f46f85 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 000000000..746deb5e3 --- /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 000000000..2746084a8 --- /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/src/sfizz/simd/HelpersSSE.cpp b/src/sfizz/simd/HelpersSSE.cpp index 079d6a752..4f8daf25b 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; @@ -370,7 +413,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 +447,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 @@ -472,3 +515,75 @@ 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); + } +} + +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 a6d5e0a55..fded80487 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; @@ -22,6 +24,8 @@ 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; +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 ab1415a7a..33c4af3f1 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 { @@ -142,7 +158,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 +170,7 @@ T meanSquaredScalar(const T* vector, unsigned size) noexcept vector++; } - return result / static_cast(size); + return result; } template @@ -186,3 +202,37 @@ 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); + } +} + +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/src/sfizz/NumericId.h b/src/sfizz/utility/NumericId.h similarity index 60% rename from src/sfizz/NumericId.h rename to src/sfizz/utility/NumericId.h index b2fed5017..73d0b12fa 100644 --- a/src/sfizz/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 @@ -18,24 +20,44 @@ 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; }; + +namespace std { + template struct hash> { + size_t operator()(const NumericId &id) const + { + return hashNumber(id.number()); + } + }; +} diff --git a/src/sfizz/utility/SpinMutex.cpp b/src/sfizz/utility/SpinMutex.cpp new file mode 100644 index 000000000..cb4160813 --- /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 000000000..5e1aa347c --- /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/src/sfizz/utility/XmlHelpers.h b/src/sfizz/utility/XmlHelpers.h new file mode 100644 index 000000000..119322993 --- /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_; +}; diff --git a/tests/ADSREnvelopeT.cpp b/tests/ADSREnvelopeT.cpp index df1044072..424d82737 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 31e134c80..eba64aeae 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,8 @@ project(sfizz) set(SFIZZ_TEST_SOURCES RegionT.cpp + TestHelpers.h + TestHelpers.cpp ParsingT.cpp HelpersT.cpp HelpersT.cpp @@ -24,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 @@ -35,11 +38,17 @@ set(SFIZZ_TEST_SOURCES SemaphoreT.cpp SwapAndPopT.cpp TuningT.cpp + ConcurrencyT.cpp + ModulationsT.cpp + LFOT.cpp + DataHelpers.h + DataHelpers.cpp ) 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) find_package(PkgConfig) @@ -90,9 +99,15 @@ 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) +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/ConcurrencyT.cpp b/tests/ConcurrencyT.cpp new file mode 100644 index 000000000..9ec72cd51 --- /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); +} diff --git a/tests/CurveT.cpp b/tests/CurveT.cpp index 4c3c9f9d9..68a88b9ca 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]); +} + diff --git a/tests/DataHelpers.cpp b/tests/DataHelpers.cpp new file mode 100644 index 000000000..a5cb7df08 --- /dev/null +++ b/tests/DataHelpers.cpp @@ -0,0 +1,90 @@ +// 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 +#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 000000000..7f337a494 --- /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/DemoWavetables.cpp b/tests/DemoWavetables.cpp index e573260bf..bd57abbe1 100644 --- a/tests/DemoWavetables.cpp +++ b/tests/DemoWavetables.cpp @@ -36,6 +36,7 @@ class DemoApp : public QApplication { private: void valueChangedWave(int value); + void valueChangedQuality(int value); void buttonClickedPlaySweep(); private: @@ -46,6 +47,7 @@ class DemoApp : public QApplication { sfz::WavetableOscillator fOsc; unsigned fWavePlaying = 0; std::atomic fNewWavePending { -1 }; + std::atomic fNewQualityPending { -1 }; std::atomic fStartNewSweep { false }; static constexpr float sweepMin = 0.0; @@ -56,6 +58,7 @@ class DemoApp : public QApplication { float fSweepIncrement = 0.0; std::unique_ptr fTmpFrequency; + std::unique_ptr fTmpDetune; jack_client_u fClient; jack_port_t* fPorts[2] = {}; @@ -86,15 +89,16 @@ 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(), 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 +132,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 +167,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( @@ -171,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; @@ -183,6 +206,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 6a5f5def1..c6347b1c0 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 @@ - + + .. diff --git a/tests/EventEnvelopesT.cpp b/tests/EventEnvelopesT.cpp index 64bcb7e41..01287d3e0 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/FileInstrument.cpp b/tests/FileInstrument.cpp index b6e21c3d0..98643be24 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; } diff --git a/tests/FileWavetable.cpp b/tests/FileWavetable.cpp new file mode 100644 index 000000000..9e1fa5972 --- /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; +} diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 8d85b23ae..333602391 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 "TestHelpers.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__) @@ -267,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.getRegionView(0)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(0)->isStereo()); - REQUIRE(synth.getRegionView(0)->isGenerator()); - REQUIRE(!synth.getRegionView(0)->oscillator); - - REQUIRE(synth.getRegionView(1)->sampleId.filename() == "*sine"); - REQUIRE(synth.getRegionView(1)->isStereo()); - REQUIRE(synth.getRegionView(1)->isGenerator()); - REQUIRE(!synth.getRegionView(1)->oscillator); - - REQUIRE(synth.getRegionView(2)->sampleId.filename() == "ramp_wave.wav"); - REQUIRE(!synth.getRegionView(2)->isStereo()); - REQUIRE(!synth.getRegionView(2)->isGenerator()); - REQUIRE(synth.getRegionView(2)->oscillator); - - REQUIRE(synth.getRegionView(3)->sampleId.filename() == "ramp_wave.wav"); - REQUIRE(synth.getRegionView(3)->isStereo()); - REQUIRE(!synth.getRegionView(3)->isGenerator()); - REQUIRE(synth.getRegionView(3)->oscillator); - - REQUIRE(synth.getRegionView(4)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(4)->isStereo()); - REQUIRE(synth.getRegionView(4)->isGenerator()); - REQUIRE(!synth.getRegionView(4)->oscillator); - - REQUIRE(synth.getRegionView(5)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(5)->isStereo()); - REQUIRE(synth.getRegionView(5)->isGenerator()); - REQUIRE(!synth.getRegionView(5)->oscillator); + REQUIRE(synth.getNumRegions() == 10); + + int regionNumber = 0; + const Region* region = nullptr; + + // 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); + + // 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); + + // 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); + + // 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); + + // 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") @@ -356,9 +412,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.valueAt(10) == 34.0f); } TEST_CASE("[Files] Specific bug: relative path with backslashes") @@ -466,55 +524,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() == 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() == 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() == 2 ); - synth.noteOn(0, 65, 63); - REQUIRE( synth.getNumActiveVoices() == 4 ); - AudioBuffer buffer { 2, 256 }; - synth.renderBlock(buffer); - synth.noteOn(0, 65, 63); - synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices() == 2 ); -} - TEST_CASE("[Files] Off modes") { Synth synth; @@ -522,7 +531,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 +541,12 @@ 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 ); + REQUIRE( numPlayingVoices(synth) == 1 ); AudioBuffer buffer { 2, 256 }; - synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices() == 2 ); + 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() ); } @@ -556,6 +567,39 @@ 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("[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"; @@ -600,11 +644,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); @@ -621,3 +663,46 @@ 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); +} + +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 oscillator=off + sample=root_key_38.wav + 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() == 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); +} diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp new file mode 100644 index 000000000..5a8412eb8 --- /dev/null +++ b/tests/FlexEGT.cpp @@ -0,0 +1,359 @@ +// 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" +#include +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() == createDefaultGraph({ + R"("EG 1 {0}" -> "Amplitude {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() == createDefaultGraph({}) ); +} + +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() == createDefaultGraph({ + 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)); +} + +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) ); +} + +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 +} + +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; + } + } +} diff --git a/tests/LFOT.cpp b/tests/LFOT.cpp new file mode 100644 index 000000000..2271c2a83 --- /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(0); + } + + 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/ModulationsT.cpp b/tests/ModulationsT.cpp new file mode 100644 index 000000000..0e1a863b2 --- /dev/null +++ b/tests/ModulationsT.cpp @@ -0,0 +1,341 @@ +// 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 "sfizz/Synth.h" +#include "TestHelpers.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((bool(flags & sfz::kModIsPerCycle) + + bool(flags & sfz::kModIsPerVoice)) == 1); + }; + static auto* checkSourceFlags = +[](int flags) + { + checkBasicFlags(flags); + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 0); + }; + static auto* checkTargetFlags = +[](int flags) + { + checkBasicFlags(flags); + REQUIRE((bool(flags & sfz::kModIsAdditive) + + bool(flags & sfz::kModIsMultiplicative) + + bool(flags & sfz::kModIsPercentMultiplicative)) == 1); + }; + + 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()); + }); +} + +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=12.5 pan_stepcc36=0.5 +width_oncc425=29 +)"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + 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=0.04}" -> "Pan {0}")", + R"("Controller 425 {curve=0, smooth=0, 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 + resonance3=-1 resonance3_oncc1=2 resonance3_smoothcc1=10 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + 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.1}" -> "FilterGain {0, N=1}")", + })); +} + +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 == createDefaultGraph({ + R"("Controller 1 {curve=0, smooth=10, step=0}" -> "EqBandwidth {0, N=3}")", + 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}")", + })); +} + +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_fil1gain=-1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + 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}")", + 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_fil1gain=-1 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + 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}")", + 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 == 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}")", + 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 == 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}")", + R"("EG 4 {0}" -> "EqBandwidth {0, N=3}")", + R"("EG 5 {0}" -> "EqGain {0, N=2}")", + 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, 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}")", + })); +} + +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, 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}")", + })); +} + + +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, 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}")", + })); +} + +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 11 {curve=4, smooth=10, step=0}" -> "Amplitude {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 11 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", + R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", + })); +} diff --git a/tests/OpcodeT.cpp b/tests/OpcodeT.cpp index 771db749b..336907f73 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) { @@ -280,3 +285,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); +} diff --git a/tests/ParsingT.cpp b/tests/ParsingT.cpp index a1e12266f..1224b46ca 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; diff --git a/tests/PlotLFO.cpp b/tests/PlotLFO.cpp new file mode 100644 index 000000000..8ce0aff5c --- /dev/null +++ b/tests/PlotLFO.cpp @@ -0,0 +1,200 @@ +// 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 "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) +{ + 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[]) +{ + 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)) + ("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"); + + 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; + } + + bool success = false; + const std::vector desc = lfoDescriptionFromSfzFile(sfzPath, 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); + + 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); + + for (size_t l = 0; l < numLfos; ++l) { + lfos[l].start(0); + } + + 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]); + } + + 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; +} diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 23716cab9..c1d891d41 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,11 +15,10 @@ using namespace sfz::literals; 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 @@ -32,19 +33,19 @@ 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") { 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,46 +72,56 @@ TEST_CASE("[Polyphony] Polyphony groups") TEST_CASE("[Polyphony] group polyphony limits") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( group=1 polyphony=2 sample=*sine key=65 )"); 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) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 )"); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 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; - synth.loadSfzString(fs::current_path(), R"( + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 )"); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices() == 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; - synth.loadSfzString(fs::current_path(), R"( + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 sample=*sine key=65 @@ -118,13 +129,16 @@ 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) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*saw key=65 @@ -137,13 +151,16 @@ 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) == 6); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 5); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 sample=*sine key=65 @@ -151,15 +168,17 @@ 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) == 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.loadSfzString(fs::current_path(), R"( + synth.setSamplesPerBlock(blockSize); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 group=2 sample=*sine key=65 @@ -171,56 +190,310 @@ 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) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing 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) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing 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 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 3 ); } TEST_CASE("[Polyphony] Self-masking") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + 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() == 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)->getTriggerEvent().value == 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)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } TEST_CASE("[Polyphony] Not self-masking") { sfz::Synth synth; - synth.loadSfzString(fs::current_path(), R"( + 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() == 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); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + 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)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 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 + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + 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)->getTriggerEvent().value == 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( 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); + 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); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); + REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 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.cc(0, 64, 127); + synth.noteOn(0, 37, 127); + synth.noteOff(0, 37, 0); + synth.noteOn(0, 48, 64); + 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 == 64_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + + +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 + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 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.cc(0, 64, 127); + synth.noteOn(0, 37, 127); + synth.noteOff(0, 37, 0); + synth.noteOn(0, 48, 64); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + 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()); +} + +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()); +} diff --git a/tests/RegionActivationT.cpp b/tests/RegionActivationT.cpp index 41990934c..e7957ffb9 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()); } } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index bce1012aa..945ae0bfc 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 "TestHelpers.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; @@ -91,7 +95,12 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "end", "184" }); REQUIRE(region.sampleEnd == 184); region.parseOpcode({ "end", "-1" }); - REQUIRE(region.sampleEnd == 0); + REQUIRE(region.disabled()); + region.parseOpcode({ "end", "2" }); + REQUIRE(!region.disabled()); + REQUIRE(region.sampleEnd == 2); + region.parseOpcode({ "end", "0" }); + REQUIRE(region.disabled()); } SECTION("count") @@ -165,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); @@ -191,6 +208,22 @@ 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); + 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" }); + region.parseOpcode({ "off_time", "-1" }); + REQUIRE(region.offTime == 0.0f); } SECTION("lokey, hikey, and key") @@ -477,6 +510,8 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.trigger == SfzTrigger::attack); region.parseOpcode({ "trigger", "release" }); REQUIRE(region.trigger == SfzTrigger::release); + region.parseOpcode({ "trigger", "release_key" }); + REQUIRE(region.trigger == SfzTrigger::release_key); region.parseOpcode({ "trigger", "first" }); REQUIRE(region.trigger == SfzTrigger::first); region.parseOpcode({ "trigger", "legato" }); @@ -541,28 +576,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.valueAt(45) == 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 +616,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.valueAt(45) == 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 +656,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.valueAt(45) == 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") @@ -669,15 +707,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") @@ -859,6 +897,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.4_a); + } + } + SECTION("pitch_keycenter") { REQUIRE(region.pitchKeycenter == 60); @@ -890,8 +968,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") @@ -902,9 +980,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") @@ -928,9 +1006,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") @@ -942,18 +1020,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" }); @@ -975,7 +1053,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); @@ -1216,6 +1294,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()); @@ -1230,9 +1321,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); @@ -1244,9 +1332,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); @@ -1259,18 +1344,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") @@ -1290,16 +1369,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); } @@ -1328,10 +1397,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" }); @@ -1347,16 +1416,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") @@ -1429,9 +1488,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); @@ -1442,9 +1498,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); @@ -1456,16 +1509,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") @@ -1495,24 +1542,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") @@ -1542,24 +1573,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") @@ -1582,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") @@ -1615,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); @@ -1623,100 +1648,98 @@ 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") { - 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.valueAt(1) == 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.valueAt(2) == 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 == 15482.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.valueAt(1) == 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.valueAt(2) == -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.valueAt(4) == -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.valueAt(1) == 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.valueAt(2) == -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.valueAt(4) == -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 == 12000.0f); region.parseOpcode({ "tune_stepcc120", "-2" }); - REQUIRE(region.modifiers[Mod::pitch][120].step == 0.0f); + REQUIRE(view.at(120).step == 0.0f); } } @@ -1736,7 +1759,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") { @@ -1752,9 +1776,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" }); @@ -1762,20 +1785,85 @@ 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 ); } } + +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 ); +} + +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)); +} diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index 9a7c9175a..bbf9b70b3 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 }; @@ -517,6 +556,7 @@ TEST_CASE("[Helpers] MultiplyAdd (SIMD)") REQUIRE(output == expected); } + TEST_CASE("[Helpers] MultiplyAdd (SIMD vs scalar)") { std::vector gain(bigBufferSize); @@ -574,6 +614,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 }; @@ -690,9 +808,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 +818,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) ); } @@ -755,60 +873,111 @@ TEST_CASE("[Helpers] Diff (SIMD vs Scalar)") REQUIRE(approxEqual(outputScalar, outputSIMD)); } -TEST_CASE("[Helpers] Pan Scalar") -{ - 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)); - } -} - -TEST_CASE("[Helpers] Width Scalar") +template +void panTest(float leftValue, float rightValue, float panValue, float expectedLeft, float expectedRight) +{ + 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) ); +} + +template +void widthTest(float leftValue, float rightValue, float widthValue, float expectedLeft, float expectedRight) +{ + 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") +{ + 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) ); +} + +TEST_CASE("[Helpers] allWithin") { - 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::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) ); + 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) ); } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index c0d761c93..b7a7d1af4 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -7,7 +7,9 @@ #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" using namespace Catch::literals; using namespace sfz::literals; @@ -23,11 +25,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 +38,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 +53,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 +133,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") @@ -200,6 +202,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 )"); @@ -445,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") @@ -491,14 +567,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 +582,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 +590,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(); @@ -538,7 +614,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 @@ -551,7 +627,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 +635,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) ); @@ -575,7 +651,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 @@ -584,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 ); } @@ -593,73 +669,714 @@ 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 )"); 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 ); - synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + for (unsigned i = 0; i < 100; ++i) + synth.renderBlock(buffer); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); 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(), 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() == 1 ); + REQUIRE( numPlayingVoices(synth) == 1 ); } 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 )"); synth.noteOn(0, 62, 85); synth.cc(0, 64, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); synth.cc(0, 64, 0); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); +} + +TEST_CASE("[Synth] Release (pedal was already down)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", 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() / "tests/TestFiles/release.sfz", 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)") { 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 )"); 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)") { 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 )"); synth.noteOn(0, 62, 85); synth.cc(0, 54, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); synth.cc(0, 54, 0); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); +} + +TEST_CASE("[Synth] Sustain threshold default") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", 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(true) == 0 ); +} + +TEST_CASE("[Synth] Sustain threshold") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", 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) == 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) == 5 ); +} + +TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", 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(true) == 3 ); + + std::vector requiredVelocities { 34_norm, 78_norm, 85_norm}; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerEvent().value); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); +} + +TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices after)") +{ + sfz::Synth synth; + 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 + )"); + 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) == 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->getTriggerEvent().value); + } + 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() / "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 + )"); + 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->getTriggerEvent().value); + } + 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() / "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 + )"); + 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->getTriggerEvent().value); + } + 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; + synth.setSamplesPerBlock(4096); + sfz::AudioBuffer buffer { 2, 4096 }; + + 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 + )"); + 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() ); +} + +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() / "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 + )"); + 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() ); +} + +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 ); +} + +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] ); +} + +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 ); +} + +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] 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; + 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 alone and repeated") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + + 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 }; + + 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) == 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" ); +} + +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)); +} + +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); } diff --git a/tests/TestFiles/channels_multi.sfz b/tests/TestFiles/channels_multi.sfz index 8146ba45d..ee6d6acf6 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 diff --git a/tests/TestFiles/root_key_38.flac b/tests/TestFiles/root_key_38.flac new file mode 100644 index 000000000..d8eefb36a Binary files /dev/null and b/tests/TestFiles/root_key_38.flac differ diff --git a/tests/TestFiles/root_key_38.wav b/tests/TestFiles/root_key_38.wav new file mode 100644 index 000000000..e7a27089c Binary files /dev/null and b/tests/TestFiles/root_key_38.wav differ diff --git a/tests/TestFiles/root_key_62.flac b/tests/TestFiles/root_key_62.flac new file mode 100644 index 000000000..10006b581 Binary files /dev/null and b/tests/TestFiles/root_key_62.flac differ diff --git a/tests/TestFiles/root_key_62.wav b/tests/TestFiles/root_key_62.wav new file mode 100644 index 000000000..658d5bd4d Binary files /dev/null and b/tests/TestFiles/root_key_62.wav differ 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 000000000..b6aafda49 Binary files /dev/null and b/tests/TestFiles/wavetable_with_loop_at_endings.wav differ diff --git a/tests/TestFiles/wavetables/clm.wav b/tests/TestFiles/wavetables/clm.wav new file mode 100644 index 000000000..07f44c423 Binary files /dev/null and b/tests/TestFiles/wavetables/clm.wav differ diff --git a/tests/TestFiles/wavetables/surge.wav b/tests/TestFiles/wavetables/surge.wav new file mode 100644 index 000000000..18ff6c51d Binary files /dev/null and b/tests/TestFiles/wavetables/surge.wav differ diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp new file mode 100644 index 000000000..0f3f76f65 --- /dev/null +++ b/tests/TestHelpers.cpp @@ -0,0 +1,126 @@ +// 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 "TestHelpers.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.source.parameters(); + if (p.cc == cc) + return p; + } + } + 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_; +} + +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; +} + +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) { + return !v->releasedOrFree(); + }); +} + +std::string createDefaultGraph(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, step=0}" -> "Amplitude {)", + regionIdx, + R"(}")" + )); + lines.push_back(absl::StrCat( + R"("Controller 10 {curve=1, smooth=10, step=0}" -> "Pan {)", + regionIdx, + R"(}")" + )); + lines.push_back(absl::StrCat( + R"("Controller 11 {curve=4, smooth=10, step=0}" -> "Amplitude {)", + regionIdx, + R"(}")" + )); + } + + return createModulationDotGraph(lines); +}; + +std::string createModulationDotGraph(std::vector lines) +{ + 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 new file mode 100644 index 000000000..4d059c456 --- /dev/null +++ b/tests/TestHelpers.h @@ -0,0 +1,96 @@ +// 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/Synth.h" +#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; + float valueAt(int cc) const; + +private: + bool match(const sfz::Region::Connection& conn) const; + +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 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 + * + * @param synth + * @return unsigned + */ +unsigned numPlayingVoices(const sfz::Synth& synth); + +/** + * @brief Create the default dot graph representation for standard regions + * + */ +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) +{ + 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; +} diff --git a/tests/WavetablesT.cpp b/tests/WavetablesT.cpp index c4d5b6835..0c6fb88ce 100644 --- a/tests/WavetablesT.cpp +++ b/tests/WavetablesT.cpp @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "sfizz/Wavetables.h" +#include "sfizz/FileMetadata.h" #include "sfizz/MathHelpers.h" #include "catch2/catch.hpp" #include @@ -12,45 +13,69 @@ 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); + REQUIRE(min_index == 0); + REQUIRE(max_index == sfz::MipmapRange::N - 1); } -TEST_CASE("[Wavetables] Octave number lookup") +TEST_CASE("[Wavetables] Wavetable sound files: Surge") { - for (int note = 0; note < 128; ++note) { - double f = midiNoteFrequency(note); + sfz::FileMetadataReader reader; + sfz::WavetableInfo wt; - float ref = std::log2(f * sfz::WavetableRange::frequencyScaleFactor); - float oct = sfz::WavetableRange::getFractionalOctaveForFrequency(f); + REQUIRE(reader.open("tests/TestFiles/wavetables/surge.wav")); + REQUIRE(reader.extractWavetableInfo(wt)); - ref = clamp(ref, 0, sfz::WavetableRange::countOctaves - 1); - oct = clamp(oct, 0, sfz::WavetableRange::countOctaves - 1); + REQUIRE(wt.tableSize == 256); +} - REQUIRE(oct == Approx(ref).margin(0.03f)); - } +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)); } diff --git a/tests/lfo/compare_lfo.py b/tests/lfo/compare_lfo.py new file mode 100755 index 000000000..31fd7e122 --- /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 000000000..122e7e942 --- /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 000000000..fa6a155f9 --- /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 000000000..6aa09f791 --- /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=0.5 +lfo1_wave=3 +// +lfo2_freq=1 +lfo2_phase=0.5 +lfo2_wave=3 +lfo2_wave2=1 +// +lfo3_freq=1 +lfo3_phase=0.5 +lfo3_wave=3 +lfo3_wave2=1 +lfo3_ratio2=2 + +// +lfo4_freq=1 +lfo4_phase=0.5 +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 000000000..c4dd0ef1a --- /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 000000000..07db6b3c8 --- /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=0.5 +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 000000000..9a65cdf25 --- /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 000000000..34c03df83 --- /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() diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index 0c14922d1..cddf56b2c 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,12 +25,8 @@ set(VSTPLUGIN_HEADERS SfizzVstController.h SfizzVstEditor.h SfizzVstState.h - GUIComponents.h X11RunLoop.h) -set(VSTPLUGIN_RESOURCES - logo.png) - add_library(${VSTPLUGIN_PRJ_NAME} MODULE ${VSTPLUGIN_HEADERS} ${VSTPLUGIN_SOURCES}) @@ -40,7 +35,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 @@ -72,26 +68,20 @@ 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}" - 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" - 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}") @@ -102,7 +92,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" @@ -154,6 +144,7 @@ elseif(SFIZZ_AU) "${APPLE_COCOA_LIBRARY}" "${APPLE_CARBON_LIBRARY}" "${APPLE_AUDIOTOOLBOX_LIBRARY}" + "${APPLE_AUDIOUNIT_LIBRARY}" "${APPLE_COREAUDIO_LIBRARY}" "${APPLE_COREMIDI_LIBRARY}") @@ -237,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}") diff --git a/vst/GUIComponents.cpp b/vst/GUIComponents.cpp deleted file mode 100644 index 314ba6421..000000000 --- a/vst/GUIComponents.cpp +++ /dev/null @@ -1,33 +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 "GUIComponents.h" -#include "vstgui/lib/cdrawcontext.h" - -SimpleSlider::SimpleSlider(const CRect& bounds, IControlListener* listener, int32_t tag) - : CSliderBase(bounds, listener, tag) -{ - 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); -} - -void SimpleSlider::draw(CDrawContext* dc) -{ - CRect bounds = getViewSize(); - CRect handle = calculateHandleRect(getValueNormalized()); - - dc->setFrameColor(_frame); - dc->drawRect(bounds, kDrawStroked); - - dc->setFillColor(_fill); - dc->drawRect(handle, kDrawFilled); -} diff --git a/vst/GUIComponents.h b/vst/GUIComponents.h deleted file mode 100644 index 003d219b7..000000000 --- a/vst/GUIComponents.h +++ /dev/null @@ -1,23 +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 "vstgui/lib/controls/cslider.h" -#include "vstgui/lib/ccolor.h" - -using namespace VSTGUI; - -class SimpleSlider : public CSliderBase { -public: - SimpleSlider(const CRect& bounds, IControlListener* listener, int32_t tag); - void draw(CDrawContext* dc) override; - - CLASS_METHODS(SimpleSlider, CSliderBase) - -private: - CColor _frame = CColor(0x00, 0x00, 0x00); - CColor _fill = CColor(0x00, 0x00, 0x00); -}; diff --git a/vst/SfizzVstController.cpp b/vst/SfizzVstController.cpp index b8c75a106..35eba7a09 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(); @@ -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 854a37221..bd363a2c9 100644 --- a/vst/SfizzVstController.h +++ b/vst/SfizzVstController.h @@ -56,6 +56,9 @@ class SfizzVstController : public SfizzVstControllerNoUi, public VSTGUI::VST3Edi 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 @@ class SfizzVstController : public SfizzVstControllerNoUi, public VSTGUI::VST3Edi private: SfizzVstState _state; SfizzUiState _uiState; + SfizzPlayState _playState {}; std::vector _stateListeners; }; diff --git a/vst/SfizzVstEditor.cpp b/vst/SfizzVstEditor.cpp index 6b35b213c..9746cdf96 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,21 +106,77 @@ void SfizzVstEditor::onStateChanged() } /// -void SfizzVstEditor::chooseSfzFile() +void SfizzVstEditor::uiSendValue(EditId id, const EditValue& v) { - SharedPointer fs(CNewFileSelector::create(frame)); + if (id == EditId::SfzFile) + loadSfzFile(v.to_string()); + else if (id == EditId::ScalaFile) + loadScalaFile(v.to_string()); + else { + SfizzVstController* ctrl = getController(); + + auto normalizeAndSet = [ctrl](Vst::ParamID pid, const SfizzParameterRange& range, float value) { + float normValue = range.normalize(value); + ctrl->setParamNormalized(pid, normValue); + ctrl->performEdit(pid, normValue); + }; + + switch (id) { + case EditId::Volume: + normalizeAndSet(kPidVolume, kParamVolumeRange, v.to_float()); + break; + case EditId::Polyphony: + normalizeAndSet(kPidNumVoices, kParamNumVoicesRange, v.to_float()); + break; + case EditId::Oversampling: + { + const int32 value = static_cast(v.to_float()); + + int32 log2Value = 0; + for (int32 f = value; f > 1; f /= 2) + ++log2Value; + + normalizeAndSet(kPidOversampling, kParamOversamplingRange, log2Value); + } + break; + case EditId::PreloadSize: + normalizeAndSet(kPidPreloadSize, kParamPreloadSizeRange, v.to_float()); + break; + case EditId::ScalaRootKey: + normalizeAndSet(kPidScalaRootKey, kParamScalaRootKeyRange, v.to_float()); + break; + case EditId::TuningFrequency: + normalizeAndSet(kPidTuningFrequency, kParamTuningFrequencyRange, v.to_float()); + break; + case EditId::StretchTuning: + normalizeAndSet(kPidStretchedTuning, kParamStretchedTuningRange, v.to_float()); + break; - fs->setTitle("Load SFZ file"); - fs->setDefaultExtension(CFileExtension("SFZ", "sfz")); + case EditId::UIActivePanel: + ctrl->getSfizzUiState().activePanel = static_cast(v.to_float()); + break; - if (fs->runModal()) { - UTF8StringPtr file = fs->getSelectedFile(0); - if (file) - loadSfzFile(file); + default: + break; + } } } -void SfizzVstEditor::loadSfzFile(const std::string& filePath) +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* data, uint32_t len) { SfizzVstController* ctl = getController(); @@ -229,26 +186,27 @@ void SfizzVstEditor::loadSfzFile(const std::string& filePath) return; } - msg->setMessageID("LoadSfz"); + msg->setMessageID("MidiMessage"); Vst::IAttributeList* attr = msg->getAttributes(); - attr->setBinary("File", filePath.data(), filePath.size()); + attr->setBinary("Data", data, len); ctl->sendMessage(msg); - - updateSfzFileLabel(filePath); } -void SfizzVstEditor::chooseScalaFile() +/// +void SfizzVstEditor::loadSfzFile(const std::string& filePath) { - SharedPointer fs(CNewFileSelector::create(frame)); - - fs->setTitle("Load Scala file"); - fs->setDefaultExtension(CFileExtension("SCL", "scl")); + SfizzVstController* ctl = getController(); - if (fs->runModal()) { - UTF8StringPtr file = fs->getSelectedFile(0); - if (file) - loadScalaFile(file); + Steinberg::OPtr msg { ctl->allocateMessage() }; + if (!msg) { + fprintf(stderr, "[Sfizz] UI could not allocate message\n"); + return; } + + msg->setMessageID("LoadSfz"); + Vst::IAttributeList* attr = msg->getAttributes(); + attr->setBinary("File", filePath.data(), filePath.size()); + ctl->sendMessage(msg); } void SfizzVstEditor::loadScalaFile(const std::string& filePath) @@ -265,280 +223,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; - } - - // 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; - 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() @@ -549,178 +233,41 @@ void SfizzVstEditor::updateStateDisplay() SfizzVstController* controller = getController(); const SfizzVstState& state = controller->getSfizzState(); const SfizzUiState& uiState = controller->getSfizzUiState(); - - 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); - - setActivePanel(uiState.activePanel); -} - -void SfizzVstEditor::updateSfzFileLabel(const std::string& filePath) -{ - 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); + const SfizzPlayState& playState = controller->getSfizzPlayState(); + + /// + 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); + + /// + 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); + + /// + uiReceiveValue(EditId::UIActivePanel, uiState.activePanel); } -void SfizzVstEditor::updateScalaRootKeyLabel(int rootKey) +Vst::ParamID SfizzVstEditor::parameterOfEditId(EditId id) { - 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 f0228b7ec..a07302207 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,89 +29,28 @@ class SfizzVstEditor : public Vst::VSTGUIEditor, public IControlListener, 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; +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* data, uint32_t len) override; + private: - void chooseSfzFile(); void loadSfzFile(const std::string& filePath); - - void chooseScalaFile(); 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()); - } - - enum { - kPanelGeneral, - // kPanelControls, - kPanelSettings, - kPanelTuning, - kNumPanels, - }; - - unsigned _activePanel = 0; - CViewContainer* _subPanels[kNumPanels] = {}; - enum { - kTagLoadSfzFile, - kTagSetVolume, - kTagSetNumVoices, - kTagSetOversampling, - kTagSetPreloadSize, - kTagLoadScalaFile, - kTagSetScalaRootKey, - kTagSetTuningFrequency, - kTagSetStretchedTuning, - kTagFirstChangePanel, - kTagLastChangePanel = kTagFirstChangePanel + kNumPanels - 1, - }; + Vst::ParamID parameterOfEditId(EditId id); - 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; + std::unique_ptr editor_; #if !defined(__APPLE__) && !defined(_WIN32) SharedPointer _runLoop; diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index 56d951e3e..6f729a271 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) { @@ -24,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); } @@ -55,6 +55,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; } @@ -127,7 +134,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(); }); @@ -143,6 +151,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); @@ -172,6 +183,8 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) else synth.disableFreeWheeling(); + processMidiFromUi(); + if (Vst::IParameterChanges* pc = data.inputParameterChanges) processControllerChanges(*pc); @@ -195,9 +208,46 @@ 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; } +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)); + } + + synth.playbackState(0, (context.state & context.kPlaying) != 0); +} + void SfizzVstProcessor::processParameterChanges(Vst::IParameterChanges& pc) { uint32 paramCount = pc.getParameterCount(); @@ -243,15 +293,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; } } @@ -327,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))); @@ -379,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; } @@ -434,6 +529,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 f215baf23..7f388621e 100644 --- a/vst/SfizzVstProcessor.h +++ b/vst/SfizzVstProcessor.h @@ -35,6 +35,7 @@ class SfizzVstProcessor : public Vst::AudioEffect { 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,12 +59,22 @@ class SfizzVstProcessor : public Vst::AudioEffect { volatile bool _workRunning = false; Ring_Buffer _fifoToWorker; RTSemaphore _semaToWorker; + Ring_Buffer _fifoMidiFromUi; std::mutex _processMutex; // file modification periodic checker uint32 _fileChangeCounter = 0; uint32 _fileChangePeriod = 0; + // state notification periodic timer + uint32 _playStateChangeCounter = 0; + uint32 _playStateChangePeriod = 0; + + // time info + int _timeSigNumerator = 0; + int _timeSigDenominator = 0; + void updateTimeInfo(const Vst::ProcessContext& context); + // messaging struct RTMessage { const char* type; diff --git a/vst/SfizzVstState.h b/vst/SfizzVstState.h index becafad33..531e2dac0 100644 --- a/vst/SfizzVstState.h +++ b/vst/SfizzVstState.h @@ -62,6 +62,15 @@ class SfizzUiState { 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; @@ -90,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 68118f73b..259f62a2a 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() diff --git a/vst/external/VST_SDK/VST3_SDK/vstgui4 b/vst/external/VST_SDK/VST3_SDK/vstgui4 deleted file mode 160000 index c6a7f607c..000000000 --- a/vst/external/VST_SDK/VST3_SDK/vstgui4 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c6a7f607c21a7353e922a6d45e54d6c56d5a6745