commit 6bb58ba41915ae7f221d6d8114e768d4b6470bbf Author: Paul Ferrand Date: Mon Jul 29 02:13:03 2019 +0200 Initial commit diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..95dc9b52 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "external/abseil-cpp"] + path = external/abseil-cpp + url = https://github.com/abseil/abseil-cpp +[submodule "external/spdlog"] + path = external/spdlog + url = https://github.com/gabime/spdlog +[submodule "external/Catch2"] + path = external/Catch2 + url = https://github.com/catchorg/Catch2 +[submodule "external/cxxopts"] + path = external/cxxopts + url = https://github.com/jarro2783/cxxopts diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..7d8289d4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,74 @@ +cmake_minimum_required(VERSION 3.14) +# You need to generate the AppConfig.h and the JuceHeader.h through the Projucer; the rest is more or less here + +project(sfizz VERSION 1.0.0 LANGUAGES CXX) + +# Set the highest possible standard +set(CMAKE_CXX_STANDARD 17) + +# Disable Abseil tests +set(BUILD_TESTING OFF) + +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT ANDROID) + add_compile_options(-stdlib=libc++) + # Presumably need the above for linking too, maybe other options missing as well + add_link_options(-stdlib=libc++) # New command on CMake master, not in 3.12 release +endif() + +add_subdirectory(external/abseil-cpp) +add_subdirectory(external/spdlog) +add_subdirectory(external/Catch2) +add_subdirectory(external/cxxopts) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(COMMON_SOURCES + sources/Opcode.cpp + sources/Synth.cpp + sources/Region.cpp + sources/Parser.cpp +) + +set(TEST_SOURCES + sources/Opcode.cpp + sources/Synth.cpp + sources/Region.cpp + sources/Parser.cpp + tests/Regex.cpp + tests/Helpers.cpp + tests/Region.cpp + tests/Range.cpp + tests/Opcode.cpp + tests/Files.cpp + tests/Main.cpp +) + + +############################### +add_executable(sfzprint sources/ParserMain.cpp ${COMMON_SOURCES}) +# Per OS properties +if(UNIX) + target_link_libraries(sfzprint stdc++fs) +endif(UNIX) +target_link_libraries(sfzprint absl::strings cxxopts) + + +############################### +# Basic command line program +add_executable(sfizz sources/Main.cpp ${COMMON_SOURCES}) +# Per OS properties +if(UNIX) + target_link_libraries(sfizz stdc++fs) +endif(UNIX) +target_link_libraries(sfizz absl::strings cxxopts) + +############################### +# Test application +add_executable(sfizz_tests ${TEST_SOURCES} ${COMMON_SOURCES}) +# Per OS properties +if(UNIX) +target_link_libraries(sfizz_tests stdc++fs) +endif(UNIX) +target_link_libraries(sfizz_tests Catch2::Catch2 absl::strings) +target_include_directories(sfizz_tests SYSTEM PRIVATE sources) +file(COPY "tests" DESTINATION ${CMAKE_BINARY_DIR}) \ No newline at end of file diff --git a/external/Catch2 b/external/Catch2 new file mode 160000 index 00000000..425957dc --- /dev/null +++ b/external/Catch2 @@ -0,0 +1 @@ +Subproject commit 425957dc63a6dd52234438c6038b901029c71372 diff --git a/external/abseil-cpp b/external/abseil-cpp new file mode 160000 index 00000000..36d37ab9 --- /dev/null +++ b/external/abseil-cpp @@ -0,0 +1 @@ +Subproject commit 36d37ab992038f52276ca66b9da80c1cf0f57dc2 diff --git a/external/cxxopts b/external/cxxopts new file mode 160000 index 00000000..3c73d91c --- /dev/null +++ b/external/cxxopts @@ -0,0 +1 @@ +Subproject commit 3c73d91c0b04e2b59462f0a741be8c07024c1bc0 diff --git a/external/spdlog b/external/spdlog new file mode 160000 index 00000000..88b4adeb --- /dev/null +++ b/external/spdlog @@ -0,0 +1 @@ +Subproject commit 88b4adebdc0dcb2be9ead196c99db60115e4d307 diff --git a/sources/Buffer.h b/sources/Buffer.h new file mode 100644 index 00000000..997deb15 --- /dev/null +++ b/sources/Buffer.h @@ -0,0 +1,20 @@ +#pragma once +#include +template +class Buffer +{ +static_assert(std::is_arithmetic::value, "Type should be arithmetic"); +public: + Buffer() + { + + } + ~Buffer() + { + + } +private: + size_t realSize; + size_t alignment; + Type* data; +}; \ No newline at end of file diff --git a/sources/CCMap.h b/sources/CCMap.h new file mode 100644 index 00000000..913b36d8 --- /dev/null +++ b/sources/CCMap.h @@ -0,0 +1,39 @@ +#include + +namespace sfz +{ +template +class CCMap +{ +public: + CCMap(const ValueType& defaultValue) + : defaultValue(defaultValue) { } + + const ValueType &getWithDefault(int index) const noexcept + { + auto it = container.find(index); + if (it == end(container)) + { + return defaultValue; + } + else + { + return it->second; + } + } + + ValueType &operator[](const int &key) noexcept + { + if (!contains(key)) + container.emplace(key, defaultValue); + return container.operator[](key); + } + + inline bool empty() const { return container.empty(); } + const ValueType &at(int index) const { return container.at(index); } + bool contains(int index) const noexcept { return container.find(index) != end(container); } +private: + const ValueType defaultValue; + std::map container; +}; +} \ No newline at end of file diff --git a/sources/Defaults.h b/sources/Defaults.h new file mode 100644 index 00000000..b7208ba0 --- /dev/null +++ b/sources/Defaults.h @@ -0,0 +1,120 @@ +#pragma once +#include "Range.h" +#include +#include + +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 SfzVelocityOverride { current, previous }; +enum class SfzCrossfadeCurve { gain, power }; + +namespace sfz +{ +namespace Default +{ + // The categories match http://sfzformat.com/ + // ******* SFZ 1 ******* + // Sound source: sample playback + inline constexpr float delay { 0.0 }; + inline constexpr float delayRandom { 0.0 }; + inline constexpr Range delayRange { 0.0, 100.0 }; + inline constexpr uint32_t offset { 0 }; + inline constexpr uint32_t offsetRandom { 0 }; + inline constexpr Range offsetRange { 0, std::numeric_limits::max() }; + inline constexpr Range sampleEndRange { 0, std::numeric_limits::max() }; + inline constexpr Range sampleCountRange { 0, std::numeric_limits::max() }; + inline constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop }; + inline constexpr Range loopRange { 0, std::numeric_limits::max() }; + + // Instrument setting: voice lifecycle + inline constexpr uint32_t group { 0 }; + inline constexpr Range groupRange { 0, std::numeric_limits::max() }; + inline constexpr SfzOffMode offMode { SfzOffMode::fast }; + + // Region logic: key mapping + inline constexpr Range keyRange { 0, 127 }; + inline constexpr Range velocityRange { 0, 127 }; + + // Region logic: MIDI conditions + inline constexpr Range channelRange { 1, 16 }; + inline constexpr Range ccRange { 0, 127 }; + inline constexpr uint8_t cc { 0 }; + inline constexpr Range bendRange { -8192, 8192 }; + inline constexpr int bend { 0 }; + inline constexpr SfzVelocityOverride velocityOverride { SfzVelocityOverride::current }; + + // Region logic: internal conditions + inline constexpr Range randRange { 0.0, 1.0 }; + inline constexpr Range aftertouchRange { 0, 127 }; + inline constexpr uint8_t aftertouch { 0 }; + inline constexpr Range bpmRange { 0.0, 500.0 }; + inline constexpr float bpm { 120.0 }; + inline constexpr uint8_t sequenceLength{ 1 }; + inline constexpr uint8_t sequencePosition{ 1 }; + inline constexpr Range sequenceRange { 1, 100 }; + + // Region logic: Triggers + inline constexpr SfzTrigger trigger { SfzTrigger::attack }; + inline constexpr Range ccTriggerValueRange{ 0, 127 }; + + // Performance parameters: amplifier + inline constexpr float volume { 0.0 }; + inline constexpr Range volumeRange { -144.0, 6.0 }; + inline constexpr Range volumeCCRange { -144.0, 6.0 }; + inline constexpr float amplitude { 100.0 }; + inline constexpr Range amplitudeRange { 0.0, 100.0 }; + inline constexpr float pan { 0.0 }; + inline constexpr Range panRange { -100.0, 100.0 }; + inline constexpr Range panCCRange { -200.0, 200.0 }; + inline constexpr float position { 0.0 }; + inline constexpr Range positionRange { -100.0, 100.0 }; + inline constexpr Range positionCCRange { -200.0, 200.0 }; + inline constexpr float width { 0.0 }; + inline constexpr Range widthRange { -100.0, 100.0 }; + inline constexpr Range widthCCRange { -200.0, 200.0 }; + inline constexpr uint8_t ampKeycenter { 60 }; + inline constexpr float ampKeytrack { 0.0 }; + inline constexpr Range ampKeytrackRange { -96, 12 }; + inline constexpr float ampVeltrack { 100.0 }; + inline constexpr Range ampVeltrackRange { -100.0, 100.0 }; + inline constexpr Range ampVelcurveRange { 0.0, 1.0 }; + inline constexpr float ampRandom { 0.0 }; + inline constexpr Range ampRandomRange { 0.0, 24.0 }; + inline constexpr Range crossfadeKeyInRange { 0, 0 }; + inline constexpr Range crossfadeKeyOutRange { 127, 127 }; + inline constexpr Range crossfadeVelInRange { 0, 0 }; + inline constexpr Range crossfadeVelOutRange { 127, 127 }; + inline constexpr SfzCrossfadeCurve crossfadeKeyCurve { SfzCrossfadeCurve::power }; + inline constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power }; + + // Performance parameters: pitch + inline constexpr uint8_t pitchKeycenter { 60 }; + inline constexpr int pitchKeytrack { 100 }; + inline constexpr Range pitchKeytrackRange { -1200, 1200 }; + inline constexpr int pitchRandom { 0 }; + inline constexpr Range pitchRandomRange { 0, 9600 }; + inline constexpr int pitchVeltrack { 0 }; + inline constexpr Range pitchVeltrackRange { -9600, 9600 }; + inline constexpr int transpose { 0 }; + inline constexpr Range transposeRange { -127, 127 }; + inline constexpr int tune { 0 }; + inline constexpr Range tuneRange { -100, 100 }; + + // Envelope generators + inline constexpr float attack { 0 }; + inline constexpr float decay { 0 }; + inline constexpr float delayEG { 0 }; + inline constexpr float hold { 0 }; + inline constexpr float release { 0 }; + inline constexpr float start { 0.0 }; + inline constexpr float sustain { 100.0 }; + inline constexpr float vel2sustain { 0.0 }; + inline constexpr int depth { 0 }; + inline constexpr Range egTimeRange { 0.0, 100.0 }; + inline constexpr Range egPercentRange { 0.0, 100.0 }; + inline constexpr Range egDepthRange { -12000, 12000 }; + inline constexpr Range egOnCCTimeRange { -100.0, 100.0 }; + inline constexpr Range egOnCCPercentRange { -100.0, 100.0 }; +} +} \ No newline at end of file diff --git a/sources/EGDescription.h b/sources/EGDescription.h new file mode 100644 index 00000000..4f7f3885 --- /dev/null +++ b/sources/EGDescription.h @@ -0,0 +1,67 @@ +#pragma once +#include "Globals.h" +#include "Defaults.h" +#include "SfzHelpers.h" +#include + + +namespace sfz +{ + +struct EGDescription +{ + float attack { Default::attack }; + float decay { Default::decay }; + float delay { Default::delayEG }; + float hold { Default::hold }; + float release { Default::release }; + float start { Default::start }; + float sustain { Default::sustain }; + int depth { Default::depth }; + float vel2attack { Default::attack }; + float vel2decay { Default::decay }; + float vel2delay { Default::delayEG }; + float vel2hold { Default::hold }; + float vel2release { Default::release }; + float vel2sustain { Default::vel2sustain }; + int vel2depth { Default::depth }; + + std::optional ccAttack; + std::optional ccDecay; + std::optional ccDelay; + std::optional ccHold; + std::optional ccRelease; + std::optional ccStart; + std::optional ccSustain; + + float getAttack(const CCValueArray &ccValues, uint8_t velocity) const noexcept + { + return ccSwitchedValue(ccValues, ccAttack, attack) + normalizeCC(velocity)*vel2attack; + } + float getDecay(const CCValueArray &ccValues, uint8_t velocity) const noexcept + { + return ccSwitchedValue(ccValues, ccDecay, decay) + normalizeCC(velocity)*vel2decay; + } + float getDelay(const CCValueArray &ccValues, uint8_t velocity) const noexcept + { + return ccSwitchedValue(ccValues, ccDelay, delay) + normalizeCC(velocity)*vel2delay; + } + float getHold(const CCValueArray &ccValues, uint8_t velocity) const noexcept + { + return ccSwitchedValue(ccValues, ccHold, hold) + normalizeCC(velocity)*vel2hold; + } + float getRelease(const CCValueArray &ccValues, uint8_t velocity) const noexcept + { + return ccSwitchedValue(ccValues, ccRelease, release) + normalizeCC(velocity)*vel2release; + } + float getStart(const CCValueArray &ccValues, uint8_t velocity [[maybe_unused]]) const noexcept + { + return ccSwitchedValue(ccValues, ccStart, start); + } + float getSustain(const CCValueArray &ccValues, uint8_t velocity) const noexcept + { + return ccSwitchedValue(ccValues, ccSustain, sustain) + normalizeCC(velocity)*vel2sustain; + } +}; + +} //namespace sfz \ No newline at end of file diff --git a/sources/Globals.h b/sources/Globals.h new file mode 100644 index 00000000..8aaa7d84 --- /dev/null +++ b/sources/Globals.h @@ -0,0 +1,21 @@ +#pragma once + +namespace sfz +{ + +namespace Config +{ + inline constexpr double defaultSampleRate { 48000 }; + inline constexpr int defaultSamplesPerBlock { 1024 }; + inline constexpr int preloadSize { 32768 }; + inline constexpr int numChannels { 2 }; + inline constexpr int numVoices { 64 }; + inline constexpr int numLoadingThreads { 4 }; + inline constexpr int centPerSemitone { 100 }; + inline constexpr float virtuallyZero { 0.00005f }; + inline constexpr double fastReleaseDuration { 0.01 }; + inline constexpr char defineCharacter { '$' }; + inline constexpr int oversamplingFactor { 2 }; +} // namespace config + +} // namespace sfz \ No newline at end of file diff --git a/sources/Helpers.h b/sources/Helpers.h new file mode 100644 index 00000000..efe93397 --- /dev/null +++ b/sources/Helpers.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include + +inline void trimInPlace(std::string_view& s) +{ + const auto leftPosition = s.find_first_not_of(" \r\t\n\f\v"); + if (leftPosition != s.npos) + { + s.remove_prefix(leftPosition); + const auto rightPosition = s.find_last_not_of(" \r\t\n\f\v"); + s.remove_suffix(s.size() - rightPosition - 1); + } + else + { + s.remove_suffix(s.size()); + } +} + +inline std::string_view trim(std::string_view s) +{ + const auto leftPosition = s.find_first_not_of(" \r\t\n\f\v"); + if (leftPosition != s.npos) + { + s.remove_prefix(leftPosition); + const auto rightPosition = s.find_last_not_of(" \r\t\n\f\v"); + s.remove_suffix(s.size() - rightPosition - 1); + } + else + { + s.remove_suffix(s.size()); + } + return s; +} + +inline constexpr unsigned int Fnv1aBasis = 0x811C9DC5; +inline constexpr unsigned int Fnv1aPrime = 0x01000193; +inline constexpr unsigned int hash(const char *s, unsigned int h = Fnv1aBasis) +{ + return !*s ? h : hash(s + 1, static_cast((h ^ *s) * static_cast(Fnv1aPrime))); +} + +inline unsigned int hash(std::string_view s, unsigned int h = Fnv1aBasis) +{ + if (s.length() > 0) + return hash(std::string_view(s.data() + 1, s.length() - 1), static_cast((h ^ s.front()) * static_cast(Fnv1aPrime))); + + return h; +} + +#ifndef NDEBUG +#if __linux__ || __unix__ + // These trap into the signal library rather than your own sourcecode + // #define ASSERTFALSE { ::kill(0, SIGTRAP); } + // #define ASSERTFALSE { raise(SIGTRAP); } + #define ASSERTFALSE { __asm__("int3"); } +#elif _WIN32 || _WIN64 + #pragma intrinsic (__debugbreak) + #define ASSERTFALSE { __debugbreak(); } +#else + #define ASSERTFALSE { __asm int 3; } +#endif +#define ASSERT(expression) if (!(expression)) ASSERTFALSE +#include +#define DBG(ostream) std::cerr << ostream << '\n' +#else +#define ASSERTFALSE +#define ASSERT(expression) +#define DBG(ostream) +#endif \ No newline at end of file diff --git a/sources/Main.cpp b/sources/Main.cpp new file mode 100644 index 00000000..809871cb --- /dev/null +++ b/sources/Main.cpp @@ -0,0 +1,37 @@ +#include "Synth.h" +#include +#include "cxxopts.hpp" + +int main(int argc, char** argv) +{ + std::ios::sync_with_stdio(false); + std::vector filesToParse; + cxxopts::Options options("sfzparser", "Parses an sfz file and prints the output"); + options.add_options()("positional", "SFZ files to parse", cxxopts::value>(filesToParse)); + options.parse_positional({"positional"}); + auto result = options.parse(argc, argv); + if (filesToParse.size() == 0) + { + std::cout << options.help() << '\n'; + return -1; + } + + sfz::Synth synth; + std::filesystem::path filename { filesToParse[0] }; + synth.loadSfzFile(filename); + std::cout << "==========" << '\n'; + std::cout << "Total:" << '\n'; + std::cout << "\tMasters: " << synth.getNumMasters() << '\n'; + std::cout << "\tGroups: " << synth.getNumGroups() << '\n'; + std::cout << "\tRegions: " << synth.getNumRegions() << '\n'; + std::cout << "\tCurves: " << synth.getNumCurves() << '\n'; + std::cout << "==========" << '\n'; + std::cout << "Included files:" << '\n'; + for (auto& file: synth.getIncludedFiles()) + std::cout << '\t' << file.c_str() << '\n'; + std::cout << "==========" << '\n'; + std::cout << "Defines:" << '\n'; + for (auto& define: synth.getDefines()) + std::cout << '\t' << define.first << '=' << define.second << '\n'; + return 0; +} \ No newline at end of file diff --git a/sources/Opcode.cpp b/sources/Opcode.cpp new file mode 100644 index 00000000..c9f51317 --- /dev/null +++ b/sources/Opcode.cpp @@ -0,0 +1,19 @@ +#include "Opcode.h" + +sfz::Opcode::Opcode(std::string_view inputOpcode, std::string_view inputValue) +:opcode(inputOpcode), value(inputValue) +{ + if (const auto lastCharIndex = inputOpcode.find_last_not_of("1234567890"); lastCharIndex != inputOpcode.npos) + { + int returnedValue; + std::string_view parameterView = inputOpcode; + parameterView.remove_prefix(lastCharIndex + 1); + if (absl::SimpleAtoi(parameterView, &returnedValue)) + { + parameter = returnedValue; + opcode.remove_suffix(opcode.size() - lastCharIndex - 1); + } + } + trimInPlace(value); + trimInPlace(opcode); +} \ No newline at end of file diff --git a/sources/Opcode.h b/sources/Opcode.h new file mode 100644 index 00000000..6285bb05 --- /dev/null +++ b/sources/Opcode.h @@ -0,0 +1,100 @@ +#pragma once +#include "Helpers.h" +#include "SfzHelpers.h" +#include "Defaults.h" +#include "Range.h" +#include +#include + +// charconv support is still sketchy with clang/gcc so we use abseil's numbers +#include "absl/strings/numbers.h" + +namespace sfz +{ +struct Opcode +{ + Opcode() = delete; + Opcode(std::string_view inputOpcode, std::string_view inputValue); + std::string_view opcode{}; + std::string_view value{}; + // This is to handle the integer parameter of some opcodes + std::optional parameter; +}; + +template +inline std::optional readOpcode(std::string_view value, const Range& validRange) +{ + if constexpr(std::is_integral::value) + { + int64_t returnedValue; + if (!absl::SimpleAtoi(value, &returnedValue)) + return {}; + + 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)); + } + else + { + float returnedValue; + if (!absl::SimpleAtof(value, &returnedValue)) + return {}; + + return validRange.clamp(returnedValue); + } +} + +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; +} + +template +inline void setValueFromOpcode(const Opcode& opcode, std::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 +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); +} + +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); +} + +template +inline void setCCPairFromOpcode(const Opcode& opcode, std::optional& target, const Range& validRange) +{ + auto value = readOpcode(opcode.value, validRange); + if (value && opcode.parameter && Default::ccRange.containsWithEnd(*opcode.parameter)) + target = std::make_pair(*opcode.parameter, *value); + else + target = {}; +} + +} diff --git a/sources/Parser.cpp b/sources/Parser.cpp new file mode 100644 index 00000000..9f895465 --- /dev/null +++ b/sources/Parser.cpp @@ -0,0 +1,136 @@ +#include "Parser.h" +#include "Helpers.h" +#include "Globals.h" +#include "absl/strings/str_join.h" +#include +#include + +using svregex_iterator = std::regex_iterator; +using svmatch_results = std::match_results; + +void removeCommentOnLine(std::string_view& line) +{ + if (auto position = line.find("//"); position != line.npos) + line.remove_suffix(line.size() - position); +} + +bool sfz::Parser::loadSfzFile(const std::filesystem::path& file) +{ + const auto sfzFile = file.is_absolute() ? file : rootDirectory / file; + if (!std::filesystem::exists(sfzFile)) + return false; + + rootDirectory = file.parent_path(); + std::vector lines; + readSfzFile(file, lines); + + aggregatedContent = absl::StrJoin(lines, " "); + const std::string_view aggregatedView { aggregatedContent }; + + svregex_iterator headerIterator(aggregatedView.cbegin(), aggregatedView.cend(), sfz::Regexes::headers); + const auto regexEnd = svregex_iterator(); + + std::vector currentMembers; + + for (; headerIterator != regexEnd; ++headerIterator) + { + svmatch_results headerMatch = *headerIterator; + + // Can't use uniform initialization here because it generates narrowing conversions + const std::string_view header(&*headerMatch[1].first, headerMatch[1].length()); + const std::string_view members(&*headerMatch[2].first, headerMatch[2].length()); + auto paramIterator = svregex_iterator (members.cbegin(), members.cend(), sfz::Regexes::members); + + // Store or handle members + for (; paramIterator != regexEnd; ++paramIterator) + { + const svmatch_results paramMatch = *paramIterator; + const std::string_view opcode(&*paramMatch[1].first, paramMatch[1].length()); + const std::string_view value(&*paramMatch[2].first, paramMatch[2].length()); + currentMembers.emplace_back(opcode, value); + } + callback(header, currentMembers); + currentMembers.clear(); + } + + return true; +} + +void sfz::Parser::readSfzFile(const std::filesystem::path& fileName, std::vector& lines) noexcept +{ + std::ifstream fileStream(fileName.c_str()); + if (!fileStream) + return; + + // spdlog::info("Including file {}", fileName.string()); + svmatch_results includeMatch; + svmatch_results defineMatch; + + std::string tmpString; + while (std::getline(fileStream, tmpString)) + { + std::string_view tmpView { tmpString }; + + removeCommentOnLine(tmpView); + trimInPlace(tmpView); + + if (tmpView.empty()) + continue; + + // New #include + if (std::regex_search(tmpView.begin(), tmpView.end(), includeMatch, sfz::Regexes::includes)) + { + auto includePath = includeMatch.str(1); + std::replace(includePath.begin(), includePath.end(), '\\', '/'); + const auto newFile = rootDirectory / includePath; + auto alreadyIncluded = std::find(includedFiles.begin(), includedFiles.end(), newFile); + if (std::filesystem::exists(newFile) && alreadyIncluded == includedFiles.end()) + { + includedFiles.push_back(newFile); + readSfzFile(newFile, lines); + } + continue; + } + + // New #define + if (std::regex_search(tmpView.begin(), tmpView.end(), defineMatch, sfz::Regexes::defines)) + { + defines[defineMatch.str(1)] = defineMatch.str(2); + continue; + } + + // Replace defined variables starting with $ + std::string newString; + newString.reserve(tmpView.length()); + std::string::size_type lastPos = 0; + std::string::size_type findPos = tmpView.find(sfz::Config::defineCharacter, lastPos); + + while(findPos < tmpView.npos) + { + newString.append(tmpView, lastPos, findPos - lastPos); + + for (auto& definePair: defines) + { + std::string_view candidate = tmpView.substr(findPos, definePair.first.length()); + if (candidate == definePair.first) + { + newString += definePair.second; + lastPos = findPos + definePair.first.length(); + break; + } + } + + if (lastPos <= findPos) + { + newString += sfz::Config::defineCharacter; + lastPos = findPos + 1; + } + + findPos = tmpView.find(sfz::Config::defineCharacter, lastPos); + } + + // Copy the rest of the string + newString += tmpView.substr(lastPos); + lines.push_back(std::move(newString)); + } +} \ No newline at end of file diff --git a/sources/Parser.h b/sources/Parser.h new file mode 100644 index 00000000..8f5d1cd8 --- /dev/null +++ b/sources/Parser.h @@ -0,0 +1,37 @@ +#pragma once +#include "Opcode.h" +#include +#include +#include +#include +#include +#include + +namespace sfz +{ +namespace Regexes +{ + inline static std::regex includes { R"V(#include\s*"(.*?)".*$)V", std::regex::optimize }; + inline static std::regex defines { R"(#define\s*(\$[a-zA-Z0-9]+)\s+([a-zA-Z0-9]+)(?=\s|$))", std::regex::optimize }; + inline static std::regex headers { R"(<(.*?)>(.*?)(?=<|$))", std::regex::optimize }; + inline static std::regex members { R"(([a-zA-Z0-9_]+)=([a-zA-Z0-9-_#.\/\s\\\(\),\*]+)(?![a-zA-Z0-9_]*=))", std::regex::optimize }; + inline static std::regex opcodeParameters{ R"(([a-zA-Z0-9_]+?)([0-9]+)$)", std::regex::optimize }; +} + +class Parser +{ +public: + virtual bool loadSfzFile(const std::filesystem::path& file); + const std::map& getDefines() const noexcept { return defines; } + const std::vector& getIncludedFiles() const noexcept { return includedFiles; } +protected: + virtual void callback(std::string_view header, std::vector members) = 0; + std::filesystem::path rootDirectory { std::filesystem::current_path() }; +private: + std::map defines; + std::vector includedFiles; + std::string aggregatedContent { }; + void readSfzFile(const std::filesystem::path& fileName, std::vector& lines) noexcept; +}; + +} // namespace sfz \ No newline at end of file diff --git a/sources/ParserMain.cpp b/sources/ParserMain.cpp new file mode 100644 index 00000000..525b3ed9 --- /dev/null +++ b/sources/ParserMain.cpp @@ -0,0 +1,77 @@ +#include "Parser.h" +#include "cxxopts.hpp" +// #include "spdlog/spdlog.h" +#include +#include +#include + + +class PrintingParser: public sfz::Parser +{ +public: + int getNumRegions() const noexcept { return numRegions; } + int getNumGroups() const noexcept { return numGroups; } + int getNumMasters() const noexcept { return numMasters; } + int getNumCurves() const noexcept { return numCurves; } +protected: + void callback(std::string_view header, std::vector members) override + { + switch (hash(header)) + { + case hash("master"): numMasters++; break; + case hash("group"): numGroups++; break; + case hash("region"): numRegions++; break; + case hash("curve"): numCurves++; break; + } + // std::cout << "[" << header << "]" << '\t'; + // for (auto& member: members) + // { + // std::cout << member.opcode << "=" << member.value; + // if (member.parameter) + // std::cout << " (" << (int)*member.parameter << ")"; + // std::cout << ' '; + + // } + // std::cout << '\n'; + } +private: + int numRegions { 0 }; + int numGroups { 0 }; + int numMasters { 0 }; + int numCurves { 0 }; +}; + +int main(int argc, char** argv) +{ + std::ios::sync_with_stdio(false); + std::vector filesToParse; + cxxopts::Options options("sfzparser", "Parses an sfz file and prints the output"); + options.add_options()("positional", "SFZ files to parse", cxxopts::value>(filesToParse)); + options.parse_positional({"positional"}); + auto result = options.parse(argc, argv); + if (filesToParse.size() == 0) + { + std::cout << options.help() << '\n'; + return -1; + } + + PrintingParser parser; + std::filesystem::path filename { filesToParse[0] }; + parser.loadSfzFile(filename); + std::cout << "==========" << '\n'; + std::cout << "Total:" << '\n'; + std::cout << "\tMasters: " << parser.getNumMasters() << '\n'; + std::cout << "\tGroups: " << parser.getNumGroups() << '\n'; + std::cout << "\tRegions: " << parser.getNumRegions() << '\n'; + std::cout << "\tCurves: " << parser.getNumCurves() << '\n'; + std::cout << "==========" << '\n'; + std::cout << "Included files:" << '\n'; + for (auto& file: parser.getIncludedFiles()) + std::cout << '\t' << file.c_str() << '\n'; + std::cout << "==========" << '\n'; + std::cout << "Defines:" << '\n'; + for (auto& define: parser.getDefines()) + std::cout << '\t' << define.first << '=' << define.second << '\n'; + // spdlog::info("Done!"); + return 0; +} \ No newline at end of file diff --git a/sources/Range.h b/sources/Range.h new file mode 100644 index 00000000..cb78896b --- /dev/null +++ b/sources/Range.h @@ -0,0 +1,75 @@ +#pragma once +#include +#include +#include + +template +class Range +{ + static_assert(std::is_arithmetic::value, "The Type should be arithmetic"); +public: + constexpr Range() = default; + // constexpr Range(std::initializer_list list) + // { + // switch(list.size()) + // { + // case 0: + // break; + // case 1: + // _start = *list.begin(); + // _end = _start; + // break; + // default: + // _start = *list.begin(); + // _end = *(list.begin() + 1); + // } + // } + constexpr Range(Type start, Type end) noexcept + : _start(start), _end(std::max(start, end)) {} + ~Range() = default; + Type start() const noexcept { return getStart(); } + Type getStart() const noexcept { return _start; } + Type end() const noexcept { return getEnd(); } + Type getEnd() const noexcept { return _end; } + std::pair getPair() const noexcept { return std::make_pair(_start, _end); } + Range(const Range& range) = default; + Range(Range&& range) = default; + void setStart(Type start) noexcept + { + _start = start; + if (start > _end) + _end = start; + } + + void setEnd(Type end) noexcept + { + _end = end; + if (end < _start) + _start = end; + } + + Type clamp(Type value) const noexcept { return std::clamp(value, _start, _end); } + bool containsWithEnd(Type value) const noexcept { return (value >= _start && value <= _end); } + bool contains(Type value) const noexcept { return (value >= _start && value < _end); } +private: + Type _start { static_cast(0.0) }; + Type _end { static_cast(0.0) }; +}; + +template +bool operator==(const Range& lhs, const Range& rhs) +{ + return (lhs.start() == rhs.start()) && (lhs.end() == rhs.end()); +} + +template +bool operator==(const Range& lhs, const std::pair& rhs) +{ + return (lhs.start() == rhs.first) && (lhs.end() == rhs.second); +} + +template +bool operator==(const std::pair& lhs, const Range& rhs) +{ + return rhs == lhs; +} \ No newline at end of file diff --git a/sources/Region.cpp b/sources/Region.cpp new file mode 100644 index 00000000..5723d19e --- /dev/null +++ b/sources/Region.cpp @@ -0,0 +1,243 @@ +#include "Region.h" +#include "Helpers.h" +#include "absl/strings/str_replace.h" + +void sfz::Region::parseOpcode(const Opcode& opcode) +{ + switch (hash(opcode.opcode)) + { + // Sound source: sample playback + case hash("sample"): + sample = absl::StrReplaceAll(trim(opcode.value), {{"\\", "/"}}); + break; + case hash("delay"): setValueFromOpcode(opcode, delay, Default::delayRange); break; + case hash("delay_random"): setValueFromOpcode(opcode, delayRandom, Default::delayRange); break; + case hash("offset"): setValueFromOpcode(opcode, offset, Default::offsetRange); break; + case hash("offset_random"): setValueFromOpcode(opcode, offsetRandom, Default::offsetRange); break; + case hash("end"): setValueFromOpcode(opcode, sampleEnd, Default::sampleEndRange); break; + case hash("count"): setValueFromOpcode(opcode, sampleCount, Default::sampleCountRange); break; + case hash("loopmode"): + case hash("loop_mode"): + switch(hash(opcode.value)) + { + case hash("no_loop"): + loopMode = SfzLoopMode::no_loop; + break; + case hash("one_shot"): + loopMode = SfzLoopMode::one_shot; + break; + case hash("loop_continuous"): + loopMode = SfzLoopMode::loop_continuous; + break; + case hash("loop_sustain"): + loopMode = SfzLoopMode::loop_sustain; + break; + default: + DBG("Unkown loop mode:" << std::string(opcode.value)); + } + break; + case hash("loopend"): + case hash("loop_end"): setRangeEndFromOpcode(opcode, loopRange, Default::loopRange); break; + case hash("loopstart"): + case hash("loop_start"): setRangeStartFromOpcode(opcode, loopRange, Default::loopRange); break; + + // Instrument settings: voice lifecycle + case hash("group"): setValueFromOpcode(opcode, group, Default::groupRange); break; + case hash("offby"): + case hash("off_by"): setValueFromOpcode(opcode, offBy, Default::groupRange); break; + case hash("off_mode"): + switch(hash(opcode.value)) + { + case hash("fast"): + offMode = SfzOffMode::fast; + break; + case hash("normal"): + offMode = SfzOffMode::normal; + break; + default: + DBG("Unkown off mode:" << std::string(opcode.value)); + } + break; + // Region logic: key mapping + case hash("lokey"): setRangeStartFromOpcode(opcode, keyRange, Default::keyRange); break; + case hash("hikey"): setRangeEndFromOpcode(opcode, keyRange, Default::keyRange); break; + case hash("key"): + setRangeStartFromOpcode(opcode, keyRange, Default::keyRange); + setRangeEndFromOpcode(opcode, keyRange, Default::keyRange); + setValueFromOpcode(opcode, pitchKeycenter, Default::keyRange); + break; + case hash("lovel"): setRangeStartFromOpcode(opcode, velocityRange, Default::velocityRange); break; + case hash("hivel"): setRangeEndFromOpcode(opcode, velocityRange, Default::velocityRange); break; + + // Region logic: MIDI conditions + case hash("lochan"): setRangeStartFromOpcode(opcode, channelRange, Default::channelRange); break; + case hash("hichan"): setRangeEndFromOpcode(opcode, channelRange, Default::channelRange); break; + case hash("lobend"): setRangeStartFromOpcode(opcode, bendRange, Default::bendRange); break; + case hash("hibend"): setRangeEndFromOpcode(opcode, bendRange, Default::bendRange); break; + case hash("locc"): + if (opcode.parameter) + setRangeStartFromOpcode(opcode, ccConditions[*opcode.parameter], Default::ccRange); + break; + case hash("hicc"): + if (opcode.parameter) + setRangeEndFromOpcode(opcode, ccConditions[*opcode.parameter], Default::ccRange); + break; + case hash("sw_lokey"): setRangeStartFromOpcode(opcode, keyswitchRange, Default::keyRange); break; + case hash("sw_hikey"): setRangeEndFromOpcode(opcode, keyswitchRange, Default::keyRange); break; + case hash("sw_last"): setValueFromOpcode(opcode, keyswitch, Default::keyRange); break; + case hash("sw_down"): setValueFromOpcode(opcode, keyswitchDown, Default::keyRange); break; + case hash("sw_up"): setValueFromOpcode(opcode, keyswitchUp, Default::keyRange); break; + case hash("sw_previous"): setValueFromOpcode(opcode, previousNote, Default::keyRange); break; + case hash("sw_vel"): + switch(hash(opcode.value)) + { + case hash("current"): + velocityOverride = SfzVelocityOverride::current; + break; + case hash("previous"): + velocityOverride = SfzVelocityOverride::previous; + break; + default: + DBG("Unknown velocity mode: " << std::string(opcode.value)); + } + break; + // Region logic: internal conditions + case hash("lochanaft"): setRangeStartFromOpcode(opcode, aftertouchRange, Default::aftertouchRange); break; + case hash("hichanaft"): setRangeEndFromOpcode(opcode, aftertouchRange, Default::aftertouchRange); break; + case hash("lobpm"): setRangeStartFromOpcode(opcode, bpmRange, Default::bpmRange); break; + case hash("hibpm"): setRangeEndFromOpcode(opcode, bpmRange, Default::bpmRange); break; + case hash("lorand"): setRangeStartFromOpcode(opcode, randRange, Default::randRange); break; + case hash("hirand"): setRangeEndFromOpcode(opcode, randRange, Default::randRange); break; + case hash("seq_length"): setValueFromOpcode(opcode, sequenceLength, Default::sequenceRange); break; + case hash("seq_position"): setValueFromOpcode(opcode, sequencePosition, Default::sequenceRange); break; + // Region logic: triggers + case hash("trigger"): + switch(hash(opcode.value)) + { + case hash("attack"): + trigger = SfzTrigger::attack; + break; + case hash("first"): + trigger = SfzTrigger::first; + break; + case hash("legato"): + trigger = SfzTrigger::legato; + break; + case hash("release"): + trigger = SfzTrigger::release; + break; + case hash("release_key"): + trigger = SfzTrigger::release_key; + break; + default: + DBG("Unknown trigger mode: " << std::string(opcode.value)); + } + break; + case hash("on_locc"): + if (opcode.parameter) + setRangeStartFromOpcode(opcode, ccTriggers[*opcode.parameter], Default::ccRange); + break; + case hash("on_hicc"): + if (opcode.parameter) + setRangeEndFromOpcode(opcode, ccTriggers[*opcode.parameter], Default::ccRange); + break; + + // Performance parameters: amplifier + case hash("volume"): setValueFromOpcode(opcode, volume, Default::volumeRange); break; + // case hash("volume_oncc"): + // if (opcode.parameter) + // setValueFromOpcode(opcode, volumeCCTriggers[*opcode.parameter], Default::volumeCCRange); + // break; + case hash("amplitude"): setValueFromOpcode(opcode, amplitude, Default::amplitudeRange); break; + case hash("amplitude_cc"): + case hash("amplitude_oncc"): setCCPairFromOpcode(opcode, amplitudeCC, Default::amplitudeRange); break; + case hash("pan"): setValueFromOpcode(opcode, pan, Default::panRange); break; + case hash("pan_oncc"): setCCPairFromOpcode(opcode, panCC, Default::panCCRange); break; + case hash("position"): setValueFromOpcode(opcode, position, Default::positionRange); break; + case hash("position_oncc"): setCCPairFromOpcode(opcode, positionCC, Default::positionCCRange); break; + case hash("width"): setValueFromOpcode(opcode, width, Default::widthRange); break; + case hash("width_oncc"): setCCPairFromOpcode(opcode, widthCC, Default::widthCCRange); break; + case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); break; + case hash("amp_keytrack"): setValueFromOpcode(opcode, ampKeytrack, Default::ampKeytrackRange); break; + case hash("amp_veltrack"): setValueFromOpcode(opcode, ampVeltrack, Default::ampVeltrackRange); break; + case hash("amp_random"): setValueFromOpcode(opcode, ampRandom, Default::ampRandomRange); break; + case hash("amp_velcurve_"): + if (opcode.parameter && Default::ccRange.containsWithEnd(*opcode.parameter)) + { + if (auto value = readOpcode(opcode.value, Default::ampVelcurveRange); value) + velocityPoints.emplace_back(*opcode.parameter, *value); + } + break; + case hash("xfin_lokey"): setRangeStartFromOpcode(opcode, crossfadeKeyInRange, Default::keyRange); break; + case hash("xfin_hikey"): setRangeEndFromOpcode(opcode, crossfadeKeyInRange, Default::keyRange); break; + case hash("xfout_lokey"): setRangeStartFromOpcode(opcode, crossfadeKeyOutRange, Default::keyRange); break; + case hash("xfout_hikey"): setRangeEndFromOpcode(opcode, crossfadeKeyOutRange, Default::keyRange); break; + case hash("xfin_lovel"): setRangeStartFromOpcode(opcode, crossfadeVelInRange, Default::velocityRange); break; + case hash("xfin_hivel"): setRangeEndFromOpcode(opcode, crossfadeVelInRange, Default::velocityRange); break; + case hash("xfout_lovel"): setRangeStartFromOpcode(opcode, crossfadeVelOutRange, Default::velocityRange); break; + case hash("xfout_hivel"): setRangeEndFromOpcode(opcode, crossfadeVelOutRange, Default::velocityRange); break; + case hash("xf_keycurve"): + switch (hash(opcode.value)) + { + case hash("power"): + crossfadeKeyCurve = SfzCrossfadeCurve::power; + break; + case hash("gain"): + crossfadeKeyCurve = SfzCrossfadeCurve::gain; + break; + default: + DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + } + break; + case hash("xf_velcurve"): + switch (hash(opcode.value)) + { + case hash("power"): + crossfadeVelCurve = SfzCrossfadeCurve::power; + break; + case hash("gain"): + crossfadeVelCurve = SfzCrossfadeCurve::gain; + break; + default: + DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + } + break; + + // Performance parameters: pitch + case hash("pitch_keycenter"): setValueFromOpcode(opcode, pitchKeycenter, Default::keyRange); break; + case hash("pitch_keytrack"): setValueFromOpcode(opcode, pitchKeytrack, Default::pitchKeytrackRange); break; + case hash("pitch_veltrack"): setValueFromOpcode(opcode, pitchVeltrack, Default::pitchVeltrackRange); break; + case hash("pitch_random"): setValueFromOpcode(opcode, pitchRandom, Default::pitchRandomRange); break; + case hash("transpose"): setValueFromOpcode(opcode, transpose, Default::transposeRange); break; + case hash("tune"): setValueFromOpcode(opcode, tune, Default::tuneRange); 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_vel2attack"): setValueFromOpcode(opcode, amplitudeEG.vel2attack, Default::egOnCCTimeRange); break; + case hash("ampeg_vel2decay"): setValueFromOpcode(opcode, amplitudeEG.vel2decay, Default::egOnCCTimeRange); break; + case hash("ampeg_vel2delay"): setValueFromOpcode(opcode, amplitudeEG.vel2delay, Default::egOnCCTimeRange); break; + case hash("ampeg_vel2hold"): setValueFromOpcode(opcode, amplitudeEG.vel2hold, Default::egOnCCTimeRange); break; + case hash("ampeg_vel2release"): setValueFromOpcode(opcode, amplitudeEG.vel2release, Default::egOnCCTimeRange); break; + case hash("ampeg_vel2sustain"): setValueFromOpcode(opcode, amplitudeEG.vel2sustain, Default::egOnCCPercentRange); break; + case hash("ampeg_attack_oncc"): setCCPairFromOpcode(opcode, amplitudeEG.ccAttack, Default::egOnCCTimeRange); break; + case hash("ampeg_decay_oncc"): setCCPairFromOpcode(opcode, amplitudeEG.ccDecay, Default::egOnCCTimeRange); break; + case hash("ampeg_delay_oncc"): setCCPairFromOpcode(opcode, amplitudeEG.ccDelay, Default::egOnCCTimeRange); break; + case hash("ampeg_hold_oncc"): setCCPairFromOpcode(opcode, amplitudeEG.ccHold, Default::egOnCCTimeRange); break; + case hash("ampeg_release_oncc"): setCCPairFromOpcode(opcode, amplitudeEG.ccRelease, Default::egOnCCTimeRange); break; + case hash("ampeg_start_oncc"): setCCPairFromOpcode(opcode, amplitudeEG.ccStart, Default::egOnCCPercentRange); break; + case hash("ampeg_sustain_oncc"): setCCPairFromOpcode(opcode, amplitudeEG.ccSustain, Default::egOnCCPercentRange); break; + // Ignored opcodes + case hash("ampeg_depth"): + case hash("ampeg_vel2depth"): + break; + default: + std::string opcodeStr { opcode.opcode.begin(), opcode.opcode.end() }; + unknownOpcodes.push_back(opcodeStr); + } +} \ No newline at end of file diff --git a/sources/Region.h b/sources/Region.h new file mode 100644 index 00000000..0301a861 --- /dev/null +++ b/sources/Region.h @@ -0,0 +1,99 @@ +#pragma once +#include +#include +#include +#include "Opcode.h" +#include "EGDescription.h" +#include "Defaults.h" +#include "CCMap.h" +namespace sfz +{ +struct Region +{ + void parseOpcode(const Opcode& opcode); + // Sound source: sample playback + std::string sample {}; // Sample + float delay { Default::delay }; // delay + float delayRandom { Default::delayRandom }; // delay_random + uint32_t offset { Default::offset }; // offset + uint32_t offsetRandom { Default::offsetRandom }; // offset_random + uint32_t sampleEnd { Default::sampleEndRange.getEnd() }; // end + std::optional sampleCount {}; // count + SfzLoopMode loopMode { Default::loopMode }; // loopmode + Range loopRange { Default::loopRange }; //loopstart and loopend + + // Instrument settings: voice lifecycle + uint32_t group { Default::group }; // group + std::optional offBy {}; // off_by + SfzOffMode offMode { Default::offMode }; // off_mode + + // Region logic: key mapping + Range keyRange{ Default::keyRange }; //lokey, hikey and key + Range velocityRange{ Default::velocityRange }; // hivel and lovel + + // Region logic: MIDI conditions + Range channelRange{ Default::channelRange }; //lochan and hichan + Range bendRange{ Default::bendRange }; // hibend and lobend + CCMap> ccConditions { Default::ccRange }; + Range keyswitchRange{ Default::keyRange }; // sw_hikey and sw_lokey + std::optional keyswitch {}; // sw_last + std::optional keyswitchUp {}; // sw_up + std::optional keyswitchDown {}; // sw_down + std::optional previousNote {}; // sw_previous + SfzVelocityOverride velocityOverride { Default::velocityOverride }; // sw_vel + + // Region logic: internal conditions + Range aftertouchRange{ Default::aftertouchRange }; // hichanaft and lochanaft + Range bpmRange{ Default::bpmRange }; // hibpm and lobpm + Range randRange{ Default::randRange }; // hirand and lorand + uint8_t sequenceLength { Default::sequenceLength }; // seq_length + uint8_t sequencePosition { Default::sequencePosition }; // seq_position + + // Region logic: triggers + SfzTrigger trigger { Default::trigger }; // trigger + std::array lastNoteVelocities; // Keeps the velocities of the previous note-ons if the region has the trigger release_key + CCMap> ccTriggers { Default::ccTriggerValueRange }; // on_loccN on_hiccN + + // Performance parameters: amplifier + float volume { Default::volume }; // volume + float amplitude { Default::amplitude }; // amplitude + float pan { Default::pan }; // pan + float width { Default::width }; // width + float position { Default::position }; // position + std::optional volumeCC; // volume_oncc + std::optional amplitudeCC; // amplitude_oncc + std::optional panCC; // pan_oncc + std::optional widthCC; // width_oncc + std::optional positionCC; // position_oncc + uint8_t ampKeycenter { Default::ampKeycenter }; // amp_keycenter + float ampKeytrack { Default::ampKeytrack }; // amp_keytrack + float ampVeltrack { Default::ampVeltrack }; // amp_keytrack + std::vector> velocityPoints; // amp_velcurve_N + float ampRandom { Default::ampRandom }; // amp_random + Range crossfadeKeyInRange { Default::crossfadeKeyInRange }; + Range crossfadeKeyOutRange { Default::crossfadeKeyOutRange }; + Range crossfadeVelInRange { Default::crossfadeVelInRange }; + Range crossfadeVelOutRange { Default::crossfadeVelOutRange }; + SfzCrossfadeCurve crossfadeKeyCurve { Default::crossfadeKeyCurve }; + SfzCrossfadeCurve crossfadeVelCurve { Default::crossfadeVelCurve }; + + + // Performance parameters: pitch + uint8_t pitchKeycenter{Default::pitchKeycenter}; // pitch_keycenter + int pitchKeytrack{ Default::pitchKeytrack }; // pitch_keytrack + int pitchRandom{ Default::pitchRandom }; // pitch_random + int pitchVeltrack{ Default::pitchVeltrack }; // pitch_veltrack + int transpose { Default::transpose }; // transpose + int tune { Default::tune }; // tune + + // Envelopes + EGDescription amplitudeEG; + EGDescription pitchEG; + EGDescription filterEG; + + double sampleRate { Config::defaultSampleRate }; + int numChannels { 1 }; + + std::vector unknownOpcodes; +}; +} // namespace sfz \ No newline at end of file diff --git a/sources/SfzHelpers.h b/sources/SfzHelpers.h new file mode 100644 index 00000000..86aa64a3 --- /dev/null +++ b/sources/SfzHelpers.h @@ -0,0 +1,185 @@ +#pragma once +#include +#include +#include +#include + +namespace sfz +{ + +using CCValueArray = std::array; +using CCValuePair = std::pair ; +using CCNamePair = std::pair; + +inline std::optional readNoteValue(const std::string_view&value) +{ + switch(hash(value)) + { + case hash("c-1"): case hash("C-1"): return (uint8_t) 0; + case hash("c#-1"): case hash("C#-1"): return (uint8_t) 1; + case hash("d-1"): case hash("D-1"): return (uint8_t) 2; + case hash("d#-1"): case hash("D#-1"): return (uint8_t) 3; + case hash("e-1"): case hash("E-1"): return (uint8_t) 4; + case hash("f-1"): case hash("F-1"): return (uint8_t) 5; + case hash("f#-1"): case hash("F#-1"): return (uint8_t) 6; + case hash("g-1"): case hash("G-1"): return (uint8_t) 7; + case hash("g#-1"): case hash("G#-1"): return (uint8_t) 8; + case hash("a-1"): case hash("A-1"): return (uint8_t) 9; + case hash("a#-1"): case hash("A#-1"): return (uint8_t) 10; + case hash("b-1"): case hash("B-1"): return (uint8_t) 11; + + case hash("c0"): case hash("C0"): return (uint8_t) 12; + case hash("c#0"): case hash("C#0"): return (uint8_t) 13; + case hash("d0"): case hash("D0"): return (uint8_t) 14; + case hash("d#0"): case hash("D#0"): return (uint8_t) 15; + case hash("e0"): case hash("E0"): return (uint8_t) 16; + case hash("f0"): case hash("F0"): return (uint8_t) 17; + case hash("f#0"): case hash("F#0"): return (uint8_t) 18; + case hash("g0"): case hash("G0"): return (uint8_t) 19; + case hash("g#0"): case hash("G#0"): return (uint8_t) 20; + case hash("a0"): case hash("A0"): return (uint8_t) 21; + case hash("a#0"): case hash("A#0"): return (uint8_t) 22; + case hash("b0"): case hash("B0"): return (uint8_t) 23; + + case hash("c1"): case hash("C1"): return (uint8_t) 24; + case hash("c#1"): case hash("C#1"): return (uint8_t) 25; + case hash("d1"): case hash("D1"): return (uint8_t) 26; + case hash("d#1"): case hash("D#1"): return (uint8_t) 27; + case hash("e1"): case hash("E1"): return (uint8_t) 28; + case hash("f1"): case hash("F1"): return (uint8_t) 29; + case hash("f#1"): case hash("F#1"): return (uint8_t) 30; + case hash("g1"): case hash("G1"): return (uint8_t) 31; + case hash("g#1"): case hash("G#1"): return (uint8_t) 32; + case hash("a1"): case hash("A1"): return (uint8_t) 33; + case hash("a#1"): case hash("A#1"): return (uint8_t) 34; + case hash("b1"): case hash("B1"): return (uint8_t) 35; + + case hash("c2"): case hash("C2"): return (uint8_t) 36; + case hash("c#2"): case hash("C#2"): return (uint8_t) 37; + case hash("d2"): case hash("D2"): return (uint8_t) 38; + case hash("d#2"): case hash("D#2"): return (uint8_t) 39; + case hash("e2"): case hash("E2"): return (uint8_t) 40; + case hash("f2"): case hash("F2"): return (uint8_t) 41; + case hash("f#2"): case hash("F#2"): return (uint8_t) 42; + case hash("g2"): case hash("G2"): return (uint8_t) 43; + case hash("g#2"): case hash("G#2"): return (uint8_t) 44; + case hash("a2"): case hash("A2"): return (uint8_t) 45; + case hash("a#2"): case hash("A#2"): return (uint8_t) 46; + case hash("b2"): case hash("B2"): return (uint8_t) 47; + + case hash("c3"): case hash("C3"): return (uint8_t) 48; + case hash("c#3"): case hash("C#3"): return (uint8_t) 49; + case hash("d3"): case hash("D3"): return (uint8_t) 50; + case hash("d#3"): case hash("D#3"): return (uint8_t) 51; + case hash("e3"): case hash("E3"): return (uint8_t) 52; + case hash("f3"): case hash("F3"): return (uint8_t) 53; + case hash("f#3"): case hash("F#3"): return (uint8_t) 54; + case hash("g3"): case hash("G3"): return (uint8_t) 55; + case hash("g#3"): case hash("G#3"): return (uint8_t) 56; + case hash("a3"): case hash("A3"): return (uint8_t) 57; + case hash("a#3"): case hash("A#3"): return (uint8_t) 58; + case hash("b3"): case hash("B3"): return (uint8_t) 59; + + case hash("c4"): case hash("C4"): return (uint8_t) 60; + case hash("c#4"): case hash("C#4"): return (uint8_t) 61; + case hash("d4"): case hash("D4"): return (uint8_t) 62; + case hash("d#4"): case hash("D#4"): return (uint8_t) 63; + case hash("e4"): case hash("E4"): return (uint8_t) 64; + case hash("f4"): case hash("F4"): return (uint8_t) 65; + case hash("f#4"): case hash("F#4"): return (uint8_t) 66; + case hash("g4"): case hash("G4"): return (uint8_t) 67; + case hash("g#4"): case hash("G#4"): return (uint8_t) 68; + case hash("a4"): case hash("A4"): return (uint8_t) 69; + case hash("a#4"): case hash("A#4"): return (uint8_t) 70; + case hash("b4"): case hash("B4"): return (uint8_t) 71; + + case hash("c5"): case hash("C5"): return (uint8_t) 72; + case hash("c#5"): case hash("C#5"): return (uint8_t) 73; + case hash("d5"): case hash("D5"): return (uint8_t) 74; + case hash("d#5"): case hash("D#5"): return (uint8_t) 75; + case hash("e5"): case hash("E5"): return (uint8_t) 76; + case hash("f5"): case hash("F5"): return (uint8_t) 77; + case hash("f#5"): case hash("F#5"): return (uint8_t) 78; + case hash("g5"): case hash("G5"): return (uint8_t) 79; + case hash("g#5"): case hash("G#5"): return (uint8_t) 80; + case hash("a5"): case hash("A5"): return (uint8_t) 81; + case hash("a#5"): case hash("A#5"): return (uint8_t) 82; + case hash("b5"): case hash("B5"): return (uint8_t) 83; + + case hash("c6"): case hash("C6"): return (uint8_t) 84; + case hash("c#6"): case hash("C#6"): return (uint8_t) 85; + case hash("d6"): case hash("D6"): return (uint8_t) 86; + case hash("d#6"): case hash("D#6"): return (uint8_t) 87; + case hash("e6"): case hash("E6"): return (uint8_t) 88; + case hash("f6"): case hash("F6"): return (uint8_t) 89; + case hash("f#6"): case hash("F#6"): return (uint8_t) 90; + case hash("g6"): case hash("G6"): return (uint8_t) 91; + case hash("g#6"): case hash("G#6"): return (uint8_t) 92; + case hash("a6"): case hash("A6"): return (uint8_t) 93; + case hash("a#6"): case hash("A#6"): return (uint8_t) 94; + case hash("b6"): case hash("B6"): return (uint8_t) 95; + + case hash("c7"): case hash("C7"): return (uint8_t) 96; + case hash("c#7"): case hash("C#7"): return (uint8_t) 97; + case hash("d7"): case hash("D7"): return (uint8_t) 98; + case hash("d#7"): case hash("D#7"): return (uint8_t) 99; + case hash("e7"): case hash("E7"): return (uint8_t) 100; + case hash("f7"): case hash("F7"): return (uint8_t) 101; + case hash("f#7"): case hash("F#7"): return (uint8_t) 102; + case hash("g7"): case hash("G7"): return (uint8_t) 103; + case hash("g#7"): case hash("G#7"): return (uint8_t) 104; + case hash("a7"): case hash("A7"): return (uint8_t) 105; + case hash("a#7"): case hash("A#7"): return (uint8_t) 106; + case hash("b7"): case hash("B7"): return (uint8_t) 107; + + case hash("c8"): case hash("C8"): return (uint8_t) 108; + case hash("c#8"): case hash("C#8"): return (uint8_t) 109; + case hash("d8"): case hash("D8"): return (uint8_t) 110; + case hash("d#8"): case hash("D#8"): return (uint8_t) 111; + case hash("e8"): case hash("E8"): return (uint8_t) 112; + case hash("f8"): case hash("F8"): return (uint8_t) 113; + case hash("f#8"): case hash("F#8"): return (uint8_t) 114; + case hash("g8"): case hash("G8"): return (uint8_t) 115; + case hash("g#8"): case hash("G#8"): return (uint8_t) 116; + case hash("a8"): case hash("A8"): return (uint8_t) 117; + case hash("a#8"): case hash("A#8"): return (uint8_t) 118; + case hash("b8"): case hash("B8"): return (uint8_t) 119; + + case hash("c9"): case hash("C9"): return (uint8_t) 120; + case hash("c#9"): case hash("C#9"): return (uint8_t) 121; + case hash("d9"): case hash("D9"): return (uint8_t) 122; + case hash("d#9"): case hash("D#9"): return (uint8_t) 123; + case hash("e9"): case hash("E9"): return (uint8_t) 124; + case hash("f9"): case hash("F9"): return (uint8_t) 125; + case hash("f#9"): case hash("F#9"): return (uint8_t) 126; + case hash("g9"): case hash("G9"): return (uint8_t) 127; + default: return {}; + } +} + +template +inline constexpr float centsFactor(T cents, T centsPerOctave = 1200) { return std::pow(2.0f, static_cast(cents) / centsPerOctave); } + +template +inline constexpr float normalizeCC(T ccValue) +{ + static_assert(std::is_integral::value); + return static_cast(std::min(std::max(ccValue, static_cast(0)), static_cast(127))) / 127.0f; +} + +template +inline constexpr float normalizePercents(T percentValue) +{ + return std::min(std::max(static_cast(percentValue), 0.0f), 100.0f) / 100.0f; +} + +inline float ccSwitchedValue(const CCValueArray& ccValues, const std::optional& ccSwitch, float value) noexcept +{ + if (ccSwitch) + return value + ccSwitch->second * normalizeCC(ccValues[ccSwitch->first]); + else + return value; +} + +} // namespace sfz + diff --git a/sources/Synth.cpp b/sources/Synth.cpp new file mode 100644 index 00000000..ca44f748 --- /dev/null +++ b/sources/Synth.cpp @@ -0,0 +1,110 @@ +#include "Synth.h" +#include "Helpers.h" +#include + +void sfz::Synth::callback(std::string_view header, std::vector members) +{ + switch (hash(header)) + { + case hash("global"): + // We shouldn't have multiple global headers in file + ASSERT(!hasGlobal); + globalOpcodes = std::move(members); + hasGlobal = true; + break; + case hash("control"): + // We shouldn't have multiple control headers in file + ASSERT(!hasControl) + hasControl = true; + break; + case hash("master"): + masterOpcodes = std::move(members); + numMasters++; + break; + case hash("group"): + groupOpcodes = std::move(members); + numGroups++; + break; + case hash("region"): + buildRegion(members); + break; + case hash("curve"): + // TODO: implement curves + numCurves++; + break; + case hash("effect"): + // TODO: implement curves + break; + default: + std::cerr << "Unknown header: " << header << '\n'; + } +} + +void sfz::Synth::buildRegion(const std::vector& regionOpcodes) +{ + auto& lastRegion = regions.emplace_back(); + for (auto& opcode: globalOpcodes) + lastRegion.parseOpcode(opcode); + for (auto& opcode: masterOpcodes) + lastRegion.parseOpcode(opcode); + for (auto& opcode: groupOpcodes) + lastRegion.parseOpcode(opcode); + for (auto& opcode: regionOpcodes) + lastRegion.parseOpcode(opcode); +} + +void sfz::Synth::clear() +{ + hasGlobal = false; + hasControl = false; + numGroups = 0; + numMasters = 0; + numCurves = 0; + defaultSwitch = std::nullopt; + for (auto& state: ccState) + state = 0; + ccNames.clear(); + globalOpcodes.clear(); + masterOpcodes.clear(); + groupOpcodes.clear(); + regions.clear(); +} + +void sfz::Synth::handleGlobalOpcodes(const std::vector& members) +{ + for (auto& member: members) + { + if (member.opcode == "sw_default") + setValueFromOpcode(member, defaultSwitch, Default::keyRange); + } +} + +void sfz::Synth::handleControlOpcodes(const std::vector& members) +{ + for (auto& member: members) + { + switch (hash(member.opcode)) + { + case hash("set_cc"): + if (member.parameter && Default::ccRange.containsWithEnd(*member.parameter)) + setValueFromOpcode(member, ccState[*member.parameter], Default::ccRange); + break; + case hash("label_cc"): + if (member.parameter && Default::ccRange.containsWithEnd(*member.parameter)) + ccNames.emplace_back(*member.parameter, member.value); + break; + case hash("default_path"): + if (auto newPath = std::filesystem::path(member.value); std::filesystem::exists(newPath)) + rootDirectory = newPath; + default: + // Unsupported control opcode + ASSERTFALSE; + } + } +} + +bool sfz::Synth::loadSfzFile(const std::filesystem::path& filename) +{ + clear(); + return sfz::Parser::loadSfzFile(filename); +} \ No newline at end of file diff --git a/sources/Synth.h b/sources/Synth.h new file mode 100644 index 00000000..aeee3797 --- /dev/null +++ b/sources/Synth.h @@ -0,0 +1,44 @@ +#pragma once +#include "Parser.h" +#include "Region.h" +#include "SfzHelpers.h" +#include +#include +#include + +namespace sfz +{ + +class Synth: public Parser +{ +public: + bool loadSfzFile(const std::filesystem::path& file) override; + int getNumRegions() const noexcept { return regions.size(); } + int getNumGroups() const noexcept { return numGroups; } + int getNumMasters() const noexcept { return numMasters; } + int getNumCurves() const noexcept { return numCurves; } + const Region* getRegionView(int idx) { return idx < regions.size() ? ®ions[idx] : nullptr; } +protected: + void callback(std::string_view header, std::vector members) override; +private: + bool hasGlobal { false }; + bool hasControl { false }; + int numGroups { 0 }; + int numMasters { 0 }; + int numCurves { 0 }; + void clear(); + void handleGlobalOpcodes(const std::vector& members); + void handleControlOpcodes(const std::vector& members); + std::vector globalOpcodes; + std::vector masterOpcodes; + std::vector groupOpcodes; + + CCValueArray ccState; + std::vector ccNames; + std::optional defaultSwitch; + + std::vector regions; + void buildRegion(const std::vector& regionOpcodes); +}; + +} \ No newline at end of file diff --git a/sources/Voice.h b/sources/Voice.h new file mode 100644 index 00000000..7fdb7ecc --- /dev/null +++ b/sources/Voice.h @@ -0,0 +1,10 @@ +#pragma once + +namespace sfz +{ +class Voice +{ +public: +private: +}; +} \ No newline at end of file diff --git a/tests/Files.cpp b/tests/Files.cpp new file mode 100644 index 00000000..ba96e76a --- /dev/null +++ b/tests/Files.cpp @@ -0,0 +1,276 @@ +#include "catch2/catch.hpp" +#include "../sources/Synth.h" +#include +using namespace Catch::literals; + +TEST_CASE("[Files] Single region (regions_one.sfz)") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Regions/regions_one.sfz"); + REQUIRE( synth.getNumRegions() == 1 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy.wav" ); +} + +TEST_CASE("[Files] Multiple regions (regions_many.sfz)") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Regions/regions_many.sfz"); + REQUIRE( synth.getNumRegions() == 3 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy.wav" ); + REQUIRE( synth.getRegionView(1)->sample == "dummy.1.wav" ); + REQUIRE( synth.getRegionView(2)->sample == "dummy.2.wav" ); +} + +TEST_CASE("[Files] Basic opcodes (regions_opcodes.sfz)") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Regions/regions_opcodes.sfz"); + REQUIRE( synth.getNumRegions() == 1 ); + REQUIRE( synth.getRegionView(0)->channelRange == Range(2, 14) ); +} + +TEST_CASE("[Files] Underscore opcodes (underscore_opcodes.sfz)") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Regions/underscore_opcodes.sfz"); + REQUIRE( synth.getNumRegions() == 1 ); + REQUIRE( synth.getRegionView(0)->loopMode == SfzLoopMode::loop_sustain ); +} + +TEST_CASE("[Files] Local include") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Includes/root_local.sfz"); + REQUIRE( synth.getNumRegions() == 1 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy.wav" ); +} + +TEST_CASE("[Files] Multiple includes") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Includes/multiple_includes.sfz"); + REQUIRE( synth.getNumRegions() == 2 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy.wav" ); + REQUIRE( synth.getRegionView(1)->sample == "dummy2.wav" ); +} + +TEST_CASE("[Files] Multiple includes with comments") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Includes/multiple_includes_with_comments.sfz"); + REQUIRE( synth.getNumRegions() == 2 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy.wav" ); + REQUIRE( synth.getRegionView(1)->sample == "dummy2.wav" ); +} + +TEST_CASE("[Files] Subdir include") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Includes/root_subdir.sfz"); + REQUIRE( synth.getNumRegions() == 1 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy_subdir.wav" ); +} + +TEST_CASE("[Files] Subdir include Win") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Includes/root_subdir_win.sfz"); + REQUIRE( synth.getNumRegions() == 1 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy_subdir.wav" ); +} + +TEST_CASE("[Files] Recursive include") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Includes/root_recursive.sfz"); + REQUIRE( synth.getNumRegions() == 2 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy_recursive2.wav" ); + REQUIRE( synth.getRegionView(1)->sample == "dummy_recursive1.wav" ); +} + +TEST_CASE("[Files] Include loops") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/Includes/root_loop.sfz"); + REQUIRE( synth.getNumRegions() == 2 ); + REQUIRE( synth.getRegionView(0)->sample == "dummy_loop2.wav" ); + REQUIRE( synth.getRegionView(1)->sample == "dummy_loop1.wav" ); +} + +TEST_CASE("[Files] Define test") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/defines.sfz"); + REQUIRE( synth.getNumRegions() == 3 ); + REQUIRE( synth.getRegionView(0)->keyRange == Range(36, 36) ); + REQUIRE( synth.getRegionView(1)->keyRange == Range(38, 38) ); + REQUIRE( synth.getRegionView(2)->keyRange == Range(42, 42) ); +} + + +TEST_CASE("[Files] Group from AVL") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/groups_avl.sfz"); + REQUIRE( synth.getNumRegions() == 5 ); + for (int i = 0; i < synth.getNumRegions(); ++i) + { + REQUIRE( synth.getRegionView(i)->volume == 6.0f ); + REQUIRE( synth.getRegionView(i)->keyRange == Range(36, 36) ); + } + REQUIRE( synth.getRegionView(0)->velocityRange == Range(1, 26) ); + REQUIRE( synth.getRegionView(1)->velocityRange == Range(27, 52) ); + REQUIRE( synth.getRegionView(2)->velocityRange == Range(53, 77) ); + REQUIRE( synth.getRegionView(3)->velocityRange == Range(78, 102) ); + REQUIRE( synth.getRegionView(4)->velocityRange == Range(103, 127) ); +} + +TEST_CASE("[Files] Full hierarchy") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); + REQUIRE( synth.getNumRegions() == 8 ); + for (int i = 0; i < synth.getNumRegions(); ++i) + { + REQUIRE( synth.getRegionView(i)->width == 40.0f ); + } + REQUIRE( synth.getRegionView(0)->pan == 30.0f ); + REQUIRE( synth.getRegionView(0)->delay == 67 ); + REQUIRE( synth.getRegionView(0)->keyRange == Range(60, 60) ); + + REQUIRE( synth.getRegionView(1)->pan == 30.0f ); + REQUIRE( synth.getRegionView(1)->delay == 67 ); + REQUIRE( synth.getRegionView(1)->keyRange == Range(61, 61) ); + + REQUIRE( synth.getRegionView(2)->pan == 30.0f ); + REQUIRE( synth.getRegionView(2)->delay == 56 ); + REQUIRE( synth.getRegionView(2)->keyRange == Range(50, 50) ); + + REQUIRE( synth.getRegionView(3)->pan == 30.0f ); + REQUIRE( synth.getRegionView(3)->delay == 56 ); + REQUIRE( synth.getRegionView(3)->keyRange == Range(51, 51) ); + + REQUIRE( synth.getRegionView(4)->pan == -10.0f ); + REQUIRE( synth.getRegionView(4)->delay == 47 ); + REQUIRE( synth.getRegionView(4)->keyRange == Range(40, 40) ); + + REQUIRE( synth.getRegionView(5)->pan == -10.0f ); + REQUIRE( synth.getRegionView(5)->delay == 47 ); + REQUIRE( synth.getRegionView(5)->keyRange == Range(41, 41) ); + + REQUIRE( synth.getRegionView(6)->pan == -10.0f ); + REQUIRE( synth.getRegionView(6)->delay == 36 ); + REQUIRE( synth.getRegionView(6)->keyRange == Range(30, 30) ); + + REQUIRE( synth.getRegionView(7)->pan == -10.0f ); + REQUIRE( synth.getRegionView(7)->delay == 36 ); + REQUIRE( synth.getRegionView(7)->keyRange == Range(31, 31) ); +} + +TEST_CASE("[Files] Reloading files") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); + REQUIRE( synth.getNumRegions() == 8 ); + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); + REQUIRE( synth.getNumRegions() == 8 ); +} + +TEST_CASE("[Files] Full hierarchy with antislashes") +{ + { + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); + REQUIRE( synth.getNumRegions() == 8 ); + REQUIRE( synth.getRegionView(0)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(1)->sample == "Regions/dummy.1.wav" ); + REQUIRE( synth.getRegionView(2)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(3)->sample == "Regions/dummy.1.wav" ); + REQUIRE( synth.getRegionView(4)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(5)->sample == "Regions/dummy.1.wav" ); + REQUIRE( synth.getRegionView(6)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(7)->sample == "Regions/dummy.1.wav" ); + } + + { + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/basic_hierarchy_antislash.sfz"); + REQUIRE( synth.getNumRegions() == 8 ); + REQUIRE( synth.getRegionView(0)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(1)->sample == "Regions/dummy.1.wav" ); + REQUIRE( synth.getRegionView(2)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(3)->sample == "Regions/dummy.1.wav" ); + REQUIRE( synth.getRegionView(4)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(5)->sample == "Regions/dummy.1.wav" ); + REQUIRE( synth.getRegionView(6)->sample == "Regions/dummy.wav" ); + REQUIRE( synth.getRegionView(7)->sample == "Regions/dummy.1.wav" ); + } +} + +TEST_CASE("[Files] Pizz basic") +{ + sfz::Synth synth; + synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/SpecificBugs/MeatBassPizz/Programs/pizz.sfz"); + REQUIRE( synth.getNumRegions() == 4 ); + for (int i = 0; i < synth.getNumRegions(); ++i) + { + REQUIRE( synth.getRegionView(i)->keyRange == Range(12, 22) ); + REQUIRE( synth.getRegionView(i)->velocityRange == Range(97, 127) ); + REQUIRE( synth.getRegionView(i)->pitchKeycenter == 21 ); + REQUIRE( synth.getRegionView(i)->ccConditions.getWithDefault(107) == Range(0, 13) ); + } + REQUIRE( synth.getRegionView(0)->randRange == Range(0, 0.25) ); + REQUIRE( synth.getRegionView(1)->randRange == Range(0.25, 0.5) ); + REQUIRE( synth.getRegionView(2)->randRange == Range(0.5, 0.75) ); + REQUIRE( synth.getRegionView(3)->randRange == Range(0.75, 1.0) ); + REQUIRE( synth.getRegionView(0)->sample == R"(../Samples/pizz/a0_vl4_rr1.wav)" ); + REQUIRE( synth.getRegionView(1)->sample == R"(../Samples/pizz/a0_vl4_rr2.wav)" ); + REQUIRE( synth.getRegionView(2)->sample == R"(../Samples/pizz/a0_vl4_rr3.wav)" ); + REQUIRE( synth.getRegionView(3)->sample == R"(../Samples/pizz/a0_vl4_rr4.wav)" ); +} + +// TEST_CASE("[Files] sw_default") +// { +// const double sampleRate { 48000 }; +// const int blockSize { 256 }; +// sfz::Synth synth; +// synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/sw_default.sfz"); +// REQUIRE( synth.getNumRegions() == 4 ); +// REQUIRE( !synth.getRegionView(0)->isSwitchedOn() ); +// REQUIRE( synth.getRegionView(1)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(2)->isSwitchedOn() ); +// REQUIRE( synth.getRegionView(3)->isSwitchedOn() ); +// } + +// TEST_CASE("[Files] sw_default and playing with switches") +// { +// const double sampleRate { 48000 }; +// const int blockSize { 256 }; +// sfz::Synth synth; +// synth.loadSfzFile(std::filesystem::current_path() / "tests/TestFiles/sw_default.sfz"); +// REQUIRE( synth.getNumRegions() == 4 ); +// REQUIRE( !synth.getRegionView(0)->isSwitchedOn() ); +// REQUIRE( synth.getRegionView(1)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(2)->isSwitchedOn() ); +// REQUIRE( synth.getRegionView(3)->isSwitchedOn() ); +// AudioBuffer buffer { 2, blockSize }; +// synth.prepareToPlay(sampleRate, blockSize); +// buffer.clear(); +// synth.registerNoteOn(1, 41, 64, 0); +// REQUIRE( synth.getRegionView(0)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(1)->isSwitchedOn() ); +// REQUIRE( synth.getRegionView(2)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(3)->isSwitchedOn() ); +// synth.registerNoteOff(1, 41, 0, 0); +// synth.registerNoteOn(1, 42, 64, 0); +// REQUIRE( !synth.getRegionView(0)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(1)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(2)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(3)->isSwitchedOn() ); +// synth.registerNoteOff(1, 42, 0, 0); +// synth.registerNoteOn(1, 40, 64, 0); +// REQUIRE( !synth.getRegionView(0)->isSwitchedOn() ); +// REQUIRE( synth.getRegionView(1)->isSwitchedOn() ); +// REQUIRE( !synth.getRegionView(2)->isSwitchedOn() ); +// REQUIRE( synth.getRegionView(3)->isSwitchedOn() ); +// } \ No newline at end of file diff --git a/tests/Helpers.cpp b/tests/Helpers.cpp new file mode 100644 index 00000000..449e94be --- /dev/null +++ b/tests/Helpers.cpp @@ -0,0 +1,63 @@ +#include "catch2/catch.hpp" +#include "../sources/Helpers.h" +#include +using namespace Catch::literals; +using namespace std::literals::string_view_literals; + +TEST_CASE("[Helpers] trimInPlace") +{ + SECTION("Trim nothing") + { + auto input { "view"sv }; + trimInPlace(input); + REQUIRE( input == "view"sv ); + } + + SECTION("Trim spaces") + { + auto input { " view "sv }; + trimInPlace(input); + REQUIRE( input == "view"sv ); + } + + SECTION("Trim other chars") + { + auto input { " \tview \t"sv }; + trimInPlace(input); + REQUIRE( input == "view"sv ); + } + + SECTION("Empty view") + { + auto input { " "sv }; + trimInPlace(input); + REQUIRE( input.empty() ); + } +} + +TEST_CASE("[Helpers] trim") +{ + SECTION("Trim nothing") + { + auto input { "view"sv }; + REQUIRE( trim(input) == "view"sv ); + } + + SECTION("Trim spaces") + { + auto input { " view "sv }; + REQUIRE( trim(input) == "view"sv ); + } + + SECTION("Trim other chars") + { + auto input { " \tview \t"sv }; + REQUIRE( trim(input) == "view"sv ); + } + + SECTION("Empty view") + { + auto input { " "sv }; + REQUIRE( trim(input).empty() ); + } +} \ No newline at end of file diff --git a/tests/Main.cpp b/tests/Main.cpp new file mode 100644 index 00000000..2e3fd313 --- /dev/null +++ b/tests/Main.cpp @@ -0,0 +1,8 @@ +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +//============================================================================== +// int main (int argc, char* argv[]) +// { +// return 0; +// } diff --git a/tests/Opcode.cpp b/tests/Opcode.cpp new file mode 100644 index 00000000..b965f604 --- /dev/null +++ b/tests/Opcode.cpp @@ -0,0 +1,62 @@ +#include "catch2/catch.hpp" +#include "../sources/Region.h" +using namespace Catch::literals; + +TEST_CASE("[Opcode] Construction") +{ + SECTION("Normal construction") + { + sfz::Opcode opcode { "sample", "dummy"}; + REQUIRE( opcode.opcode == "sample" ); + REQUIRE( opcode.value == "dummy" ); + REQUIRE( !opcode.parameter ); + } + + SECTION("Normal construction with underscore") + { + sfz::Opcode opcode { "sample_underscore", "dummy"}; + REQUIRE( opcode.opcode == "sample_underscore" ); + REQUIRE( opcode.value == "dummy" ); + REQUIRE( !opcode.parameter ); + } + + SECTION("Parameterized opcode") + { + sfz::Opcode opcode { "sample123", "dummy"}; + REQUIRE( opcode.opcode == "sample" ); + REQUIRE( opcode.value == "dummy" ); + REQUIRE( opcode.parameter ); + REQUIRE( *opcode.parameter == 123 ); + } + + SECTION("Parameterized opcode with underscore") + { + sfz::Opcode opcode { "sample_underscore123", "dummy"}; + REQUIRE( opcode.opcode == "sample_underscore" ); + REQUIRE( opcode.value == "dummy" ); + REQUIRE( opcode.parameter ); + REQUIRE( *opcode.parameter == 123 ); + } +} + +TEST_CASE("[Opcode] Note values") +{ + auto noteValue = sfz::readNoteValue("c-1"); + REQUIRE( noteValue ); + REQUIRE( *noteValue == 0); + noteValue = sfz::readNoteValue("C-1"); + REQUIRE( noteValue ); + REQUIRE( *noteValue == 0); + noteValue = sfz::readNoteValue("g9"); + REQUIRE( noteValue ); + REQUIRE( *noteValue == 127); + noteValue = sfz::readNoteValue("G9"); + REQUIRE( noteValue ); + REQUIRE( *noteValue == 127); + noteValue = sfz::readNoteValue("c#4"); + REQUIRE( noteValue ); + REQUIRE( *noteValue == 61); + noteValue = sfz::readNoteValue("C#4"); + REQUIRE( noteValue ); + REQUIRE( *noteValue == 61); +} diff --git a/tests/Range.cpp b/tests/Range.cpp new file mode 100644 index 00000000..fc39c7de --- /dev/null +++ b/tests/Range.cpp @@ -0,0 +1,74 @@ +#include "Range.h" +#include "catch2/catch.hpp" +using namespace Catch::literals; + +TEST_CASE("[Range] Equality operators") +{ + Range intRange {1, 1}; + REQUIRE( intRange == Range(1, 1) ); + REQUIRE( intRange == std::pair(1, 1) ); + REQUIRE( std::pair(1, 1) == intRange ); + + Range floatRange {1.0f, 1.0f}; + REQUIRE( floatRange == Range(1.0f, 1.0f) ); + REQUIRE( floatRange == std::pair(1.0f, 1.0f) ); + REQUIRE( std::pair(1.0f, 1.0f) == floatRange); +} + +TEST_CASE("[Range] Default ranges for classical types") +{ + Range intRange; + REQUIRE( intRange == Range(0, 0) ); + Range int32_tRange; + REQUIRE( int32_tRange == Range(0, 0) ); + Range uint32_tRange; + REQUIRE( uint32_tRange == Range(0, 0) ); + Range int64_tRange; + REQUIRE( int64_tRange == Range(0, 0) ); + Range uint64_tRange; + REQUIRE( uint64_tRange == Range(0, 0) ); + Range floatRange; + REQUIRE( floatRange == Range(0, 0) ); + Range doubleRange; + REQUIRE( doubleRange == Range(0, 0) ); +} + +TEST_CASE("[Range] Contains") +{ + Range intRange {1, 10}; + REQUIRE( !intRange.contains(0) ); + REQUIRE( intRange.contains(1) ); + REQUIRE( intRange.contains(5) ); + REQUIRE( !intRange.contains(10) ); + REQUIRE( !intRange.containsWithEnd(0) ); + REQUIRE( intRange.containsWithEnd(1) ); + REQUIRE( intRange.containsWithEnd(5) ); + REQUIRE( intRange.containsWithEnd(10) ); + + Range floatRange {1.0, 10.0}; + REQUIRE( !floatRange.contains(0.0) ); + REQUIRE( floatRange.contains(1.0) ); + REQUIRE( floatRange.contains(5.0) ); + REQUIRE( !floatRange.contains(10.0) ); + REQUIRE( !floatRange.containsWithEnd(0.0) ); + REQUIRE( floatRange.containsWithEnd(1.0) ); + REQUIRE( floatRange.containsWithEnd(5.0) ); + REQUIRE( floatRange.containsWithEnd(10.0) ); +} + +TEST_CASE("[Range] Clamp") +{ + Range intRange {1, 10}; + REQUIRE( intRange.clamp(0) == 1 ); + REQUIRE( intRange.clamp(1) == 1 ); + REQUIRE( intRange.clamp(5) == 5 ); + REQUIRE( intRange.clamp(10) == 10 ); + REQUIRE( intRange.clamp(11) == 10 ); + + Range floatRange {1.0, 10.0}; + REQUIRE( floatRange.clamp(0.0) == 1.0_a ); + REQUIRE( floatRange.clamp(1.0) == 1.0_a ); + REQUIRE( floatRange.clamp(5.0) == 5.0_a ); + REQUIRE( floatRange.clamp(10.0) == 10.0_a ); + REQUIRE( floatRange.clamp(11.0) == 10.0_a ); +} \ No newline at end of file diff --git a/tests/Regex.cpp b/tests/Regex.cpp new file mode 100644 index 00000000..ff4f52e1 --- /dev/null +++ b/tests/Regex.cpp @@ -0,0 +1,132 @@ +#include "catch2/catch.hpp" +#include "../sources/Parser.h" +using namespace Catch::literals; + +void includeTest(const std::string& line, const std::string& fileName) +{ + std::smatch includeMatch; + auto found = std::regex_search(line, includeMatch, sfz::Regexes::includes); + REQUIRE(found); + REQUIRE(includeMatch[1] == fileName); +} + +TEST_CASE("[Regex] #include") +{ + includeTest("#include \"file.sfz\"", "file.sfz"); + includeTest("#include \"../Programs/file.sfz\"", "../Programs/file.sfz"); + includeTest("#include \"..\\Programs\\file.sfz\"", "..\\Programs\\file.sfz"); + includeTest("#include \"file-1.sfz\"", "file-1.sfz"); + includeTest("#include \"file~1.sfz\"", "file~1.sfz"); + includeTest("#include \"file_1.sfz\"", "file_1.sfz"); + includeTest("#include \"file$1.sfz\"", "file$1.sfz"); + includeTest("#include \"file,1.sfz\"", "file,1.sfz"); + includeTest("#include \"rubbishCharactersAfter.sfz\" blabldaljf///df", "rubbishCharactersAfter.sfz"); + includeTest("#include \"lazyMatching.sfz\" b\"", "lazyMatching.sfz"); +} + +void defineTest(const std::string& line, const std::string& variable, const std::string& value) +{ + std::smatch defineMatch; + auto found = std::regex_search(line, defineMatch, sfz::Regexes::defines); + REQUIRE(found); + REQUIRE(defineMatch[1] == variable); + REQUIRE(defineMatch[2] == value); +} + +void defineFail(const std::string& line) +{ + std::smatch defineMatch; + auto found = std::regex_search(line, defineMatch, sfz::Regexes::defines); + REQUIRE(!found); +} + +TEST_CASE("[Regex] #define") +{ + defineTest("#define $number 1", "$number", "1"); + defineTest("#define $letters QWERasdf", "$letters", "QWERasdf"); + defineTest("#define $alphanum asr1t44", "$alphanum", "asr1t44"); + defineTest("#define $whitespace asr1t44 ", "$whitespace", "asr1t44"); + defineTest("#define $lazyMatching matched bfasd ", "$lazyMatching", "matched"); + defineFail("#define $symbols# 1"); + defineFail("#define $symbolsAgain $1"); + defineFail("#define $trailingSymbols 1$"); +} + +TEST_CASE("[Regex] Header") +{ + SECTION("Basic header match") + { + std::smatch headerMatch; + std::string line{"
param1=value1 param2=value2"}; + auto found = std::regex_search(line, headerMatch, sfz::Regexes::headers); + REQUIRE(found); + REQUIRE(headerMatch[1] == "header"); + REQUIRE(headerMatch[2] == "param1=value1 param2=value2"); + } + SECTION("EOL header match") + { + std::smatch headerMatch; + std::string line{"
param1=value1 param2=value2"}; + auto found = std::regex_search(line, headerMatch, sfz::Regexes::headers); + REQUIRE(found); + REQUIRE(headerMatch[1] == "header"); + REQUIRE(headerMatch[2] == "param1=value1 param2=value2"); + } +} + +void memberTest(const std::string &line, const std::string &variable, const std::string &value) +{ + std::smatch memberMatch; + auto found = std::regex_search(line, memberMatch, sfz::Regexes::members); + REQUIRE(found); + REQUIRE(memberMatch[1] == variable); + REQUIRE(memberMatch[2] == value); +} + +TEST_CASE("[Regex] Member") +{ + memberTest("param=value", "param", "value"); + memberTest("param=113", "param", "113"); + memberTest("param1=value", "param1", "value"); + memberTest("param_1=value", "param_1", "value"); + memberTest("param_1=value", "param_1", "value"); + memberTest("ampeg_sustain_oncc74=-100", "ampeg_sustain_oncc74", "-100"); + memberTest("lorand=0.750", "lorand", "0.750"); + memberTest("sample=value", "sample", "value"); + memberTest("sample=value-()*", "sample", "value-()*"); + memberTest("sample=../sample.wav", "sample", "../sample.wav"); + memberTest("sample=..\\sample.wav", "sample", "..\\sample.wav"); + memberTest("sample=subdir\\subdir\\sample.wav", "sample", "subdir\\subdir\\sample.wav"); + memberTest("sample=subdir/subdir/sample.wav", "sample", "subdir/subdir/sample.wav"); + memberTest("sample=subdir_underscore\\sample.wav", "sample", "subdir_underscore\\sample.wav"); + memberTest("sample=subdir space\\sample.wav", "sample", "subdir space\\sample.wav"); + memberTest("sample=subdir space\\sample.wav next_member=value", "sample", "subdir space\\sample.wav"); + memberTest("sample=..\\Samples\\pizz\\a0_vl3_rr3.wav", "sample", "..\\Samples\\pizz\\a0_vl3_rr3.wav"); + memberTest("sample=..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav", "sample", "..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav"); +} + +void parameterTest(const std::string& line, const std::string& opcode, const std::string& parameter) +{ + std::smatch parameterMatch; + auto found = std::regex_search(line, parameterMatch, sfz::Regexes::opcodeParameters); + REQUIRE(found); + REQUIRE(parameterMatch[1] == opcode); + REQUIRE(parameterMatch[2] == parameter); +} + +void parameterFail(const std::string &line) +{ + std::smatch parameterMatch; + auto found = std::regex_search(line, parameterMatch, sfz::Regexes::opcodeParameters); + REQUIRE(!found); +} + +TEST_CASE("[Regex] Opcode parameter") +{ + parameterTest("opcode_123", "opcode_", "123"); + parameterTest("xfin_locc1", "xfin_locc", "1"); + parameterTest("ampeg_hold_oncc24", "ampeg_hold_oncc", "24"); + parameterTest("lfo02_phase_oncc135", "lfo02_phase_oncc", "135"); + parameterFail("lfo01_freq"); + parameterFail("ampeg_sustain"); +} \ No newline at end of file diff --git a/tests/Region.cpp b/tests/Region.cpp new file mode 100644 index 00000000..dc2a0649 --- /dev/null +++ b/tests/Region.cpp @@ -0,0 +1,940 @@ +#include "catch2/catch.hpp" +#include "../sources/Region.h" +using namespace Catch::literals; + +TEST_CASE("[Region] Parsing opcodes") +{ + sfz::Region region; + SECTION("sample") + { + REQUIRE( region.sample == "" ); + region.parseOpcode({ "sample", "dummy.wav" }); + REQUIRE( region.sample == "dummy.wav" ); + } + + SECTION("delay") + { + REQUIRE( region.delay == 0.0 ); + region.parseOpcode({ "delay", "1.0" }); + REQUIRE( region.delay == 1.0 ); + region.parseOpcode({ "delay", "-1.0" }); + REQUIRE( region.delay == 0.0 ); + region.parseOpcode({ "delay", "110.0" }); + REQUIRE( region.delay == 100.0 ); + } + + SECTION("delay_random") + { + REQUIRE( region.delayRandom == 0.0 ); + region.parseOpcode({ "delay_random", "1.0" }); + REQUIRE( region.delayRandom == 1.0); + region.parseOpcode({ "delay_random", "-1.0" }); + REQUIRE( region.delayRandom == 0.0 ); + region.parseOpcode({ "delay_random", "110.0" }); + REQUIRE( region.delayRandom == 100.0 ); + } + + SECTION("offset") + { + REQUIRE( region.offset == 0 ); + region.parseOpcode({ "offset", "1" }); + REQUIRE( region.offset == 1 ); + region.parseOpcode({ "offset", "-1" }); + REQUIRE( region.offset == 0 ); + } + + SECTION("offset_random") + { + REQUIRE( region.offsetRandom == 0 ); + region.parseOpcode({ "offset_random", "1" }); + REQUIRE( region.offsetRandom == 1 ); + region.parseOpcode({ "offset_random", "-1" }); + REQUIRE( region.offsetRandom == 0); + } + + SECTION("end") + { + region.parseOpcode({ "end", "184" }); + REQUIRE( region.sampleEnd == 184 ); + region.parseOpcode({ "end", "-1" }); + REQUIRE( region.sampleEnd == 0 ); + } + + SECTION("count") + { + REQUIRE( !region.sampleCount ); + region.parseOpcode({ "count", "184" }); + REQUIRE( region.sampleCount ); + REQUIRE( *region.sampleCount == 184 ); + region.parseOpcode({ "count", "-1" }); + REQUIRE( region.sampleCount ); + REQUIRE( *region.sampleCount == 0 ); + } + + SECTION("loop_mode") + { + REQUIRE( region.loopMode == SfzLoopMode::no_loop ); + region.parseOpcode({ "loop_mode", "no_loop" }); + REQUIRE( region.loopMode == SfzLoopMode::no_loop ); + region.parseOpcode({ "loop_mode", "one_shot" }); + REQUIRE( region.loopMode == SfzLoopMode::one_shot ); + region.parseOpcode({ "loop_mode", "loop_continuous" }); + REQUIRE( region.loopMode == SfzLoopMode::loop_continuous ); + region.parseOpcode({ "loop_mode", "loop_sustain" }); + REQUIRE( region.loopMode == SfzLoopMode::loop_sustain ); + } + + SECTION("loopmode") + { + REQUIRE( region.loopMode == SfzLoopMode::no_loop ); + region.parseOpcode({ "loopmode", "no_loop" }); + REQUIRE( region.loopMode == SfzLoopMode::no_loop ); + region.parseOpcode({ "loopmode", "one_shot" }); + REQUIRE( region.loopMode == SfzLoopMode::one_shot ); + region.parseOpcode({ "loopmode", "loop_continuous" }); + REQUIRE( region.loopMode == SfzLoopMode::loop_continuous ); + region.parseOpcode({ "loopmode", "loop_sustain" }); + REQUIRE( region.loopMode == SfzLoopMode::loop_sustain ); + } + + SECTION("loop_end") + { + REQUIRE( region.loopRange == Range(0, 4294967295) ); + region.parseOpcode({ "loop_end", "184" }); + REQUIRE( region.loopRange == Range(0, 184) ); + region.parseOpcode({ "loop_end", "-1" }); + REQUIRE( region.loopRange == Range(0, 0) ); + } + + SECTION("loop_start") + { + region.parseOpcode({ "loop_start", "184" }); + REQUIRE( region.loopRange == Range(184, 4294967295) ); + region.parseOpcode({ "loop_start", "-1" }); + REQUIRE( region.loopRange == Range(0, 4294967295) ); + } + + SECTION("loopend") + { + REQUIRE( region.loopRange == Range(0, 4294967295) ); + region.parseOpcode({ "loopend", "184" }); + REQUIRE( region.loopRange == Range(0, 184) ); + region.parseOpcode({ "loopend", "-1" }); + REQUIRE( region.loopRange == Range(0, 0) ); + } + + SECTION("loopstart") + { + region.parseOpcode({ "loopstart", "184" }); + REQUIRE( region.loopRange == Range(184, 4294967295) ); + region.parseOpcode({ "loopstart", "-1" }); + REQUIRE( region.loopRange == Range(0, 4294967295) ); + } + + SECTION("group") + { + REQUIRE( region.group == 0 ); + region.parseOpcode({ "group", "5" }); + REQUIRE( region.group == 5 ); + region.parseOpcode({ "group", "-1" }); + REQUIRE( region.group == 0 ); + } + + SECTION("off_by") + { + REQUIRE( !region.offBy ); + region.parseOpcode({ "off_by", "5" }); + REQUIRE( region.offBy ); + REQUIRE( region.offBy == 5 ); + region.parseOpcode({ "off_by", "-1" }); + REQUIRE( region.offBy ); + REQUIRE( region.offBy == 0 ); + } + + SECTION("off_mode") + { + REQUIRE( region.offMode == SfzOffMode::fast ); + region.parseOpcode({ "off_mode", "fast" }); + REQUIRE( region.offMode == SfzOffMode::fast ); + region.parseOpcode({ "off_mode", "normal" }); + REQUIRE( region.offMode == SfzOffMode::normal ); + } + + SECTION("lokey, hikey, and key") + { + REQUIRE( region.keyRange == Range(0, 127) ); + region.parseOpcode({ "lokey", "37" }); + REQUIRE( region.keyRange == Range(37, 127) ); + region.parseOpcode({ "lokey", "c4" }); + REQUIRE( region.keyRange == Range(60, 127) ); + region.parseOpcode({ "lokey", "128" }); + REQUIRE( region.keyRange == Range(127, 127) ); + region.parseOpcode({ "lokey", "-3" }); + REQUIRE( region.keyRange == Range(0, 127) ); + region.parseOpcode({ "hikey", "65" }); + REQUIRE( region.keyRange == Range(0, 65) ); + region.parseOpcode({ "hikey", "c4" }); + REQUIRE( region.keyRange == Range(0, 60) ); + region.parseOpcode({ "hikey", "-1" }); + REQUIRE( region.keyRange == Range(0, 0) ); + region.parseOpcode({ "hikey", "128" }); + REQUIRE( region.keyRange == Range(0, 127) ); + region.parseOpcode({ "key", "26" }); + REQUIRE( region.keyRange == Range(26, 26) ); + REQUIRE( region.pitchKeycenter == 26 ); + region.parseOpcode({ "key", "-26" }); + REQUIRE( region.keyRange == Range(0, 0) ); + REQUIRE( region.pitchKeycenter == 0 ); + region.parseOpcode({ "key", "234" }); + REQUIRE( region.keyRange == Range(127, 127) ); + REQUIRE( region.pitchKeycenter == 127 ); + region.parseOpcode({ "key", "c4" }); + REQUIRE( region.keyRange == Range(60, 60) ); + REQUIRE( region.pitchKeycenter == 60 ); + } + + SECTION("lovel, hivel") + { + REQUIRE( region.velocityRange == Range(0, 127) ); + region.parseOpcode({ "lovel", "37" }); + REQUIRE( region.velocityRange == Range(37, 127) ); + region.parseOpcode({ "lovel", "128" }); + REQUIRE( region.velocityRange == Range(127, 127) ); + region.parseOpcode({ "lovel", "-3" }); + REQUIRE( region.velocityRange == Range(0, 127) ); + region.parseOpcode({ "hivel", "65" }); + REQUIRE( region.velocityRange == Range(0, 65) ); + region.parseOpcode({ "hivel", "-1" }); + REQUIRE( region.velocityRange == Range(0, 0) ); + region.parseOpcode({ "hivel", "128" }); + REQUIRE( region.velocityRange == Range(0, 127) ); + } + + SECTION("lochan, hichan") + { + REQUIRE( region.channelRange == Range(1, 16) ); + region.parseOpcode({ "lochan", "4" }); + REQUIRE( region.channelRange == Range(4, 16) ); + region.parseOpcode({ "lochan", "128" }); + REQUIRE( region.channelRange == Range(16, 16) ); + region.parseOpcode({ "lochan", "-3" }); + REQUIRE( region.channelRange == Range(1, 16) ); + region.parseOpcode({ "hichan", "13" }); + REQUIRE( region.channelRange == Range(1, 13) ); + region.parseOpcode({ "hichan", "-1" }); + REQUIRE( region.channelRange == Range(1, 1) ); + region.parseOpcode({ "hichan", "128" }); + REQUIRE( region.channelRange == Range(1, 16) ); + } + + SECTION("lobend, hibend") + { + REQUIRE( region.bendRange == Range(-8192, 8192) ); + region.parseOpcode({ "lobend", "4" }); + REQUIRE( region.bendRange == Range(4, 8192) ); + region.parseOpcode({ "lobend", "-128" }); + REQUIRE( region.bendRange == Range(-128, 8192) ); + region.parseOpcode({ "lobend", "-10000" }); + REQUIRE( region.bendRange == Range(-8192, 8192) ); + region.parseOpcode({ "hibend", "13" }); + REQUIRE( region.bendRange == Range(-8192, 13) ); + region.parseOpcode({ "hibend", "-1" }); + REQUIRE( region.bendRange == Range(-8192, -1) ); + region.parseOpcode({ "hibend", "10000" }); + REQUIRE( region.bendRange == Range(-8192, 8192) ); + } + + SECTION("locc, hicc") + { + REQUIRE( region.ccConditions.getWithDefault(0) == Range(0, 127) ); + REQUIRE( region.ccConditions[127] == Range(0, 127) ); + region.parseOpcode({ "locc6", "4" }); + REQUIRE( region.ccConditions[6] == Range(4, 127) ); + region.parseOpcode({ "locc12", "-128" }); + REQUIRE( region.ccConditions[12] == Range(0, 127) ); + region.parseOpcode({ "hicc65", "39" }); + REQUIRE( region.ccConditions[65] == Range(0, 39) ); + region.parseOpcode({ "hicc127", "135" }); + REQUIRE( region.ccConditions[127] == Range(0, 127) ); + } + + SECTION("sw_lokey, sw_hikey") + { + REQUIRE( region.keyswitchRange == Range(0, 127) ); + region.parseOpcode({ "sw_lokey", "4" }); + REQUIRE( region.keyswitchRange == Range(4, 127) ); + region.parseOpcode({ "sw_lokey", "128" }); + REQUIRE( region.keyswitchRange == Range(127, 127) ); + region.parseOpcode({ "sw_lokey", "0" }); + REQUIRE( region.keyswitchRange == Range(0, 127) ); + region.parseOpcode({ "sw_hikey", "39" }); + REQUIRE( region.keyswitchRange == Range(0, 39) ); + region.parseOpcode({ "sw_hikey", "135" }); + REQUIRE( region.keyswitchRange == Range(0, 127) ); + region.parseOpcode({ "sw_hikey", "-1" }); + REQUIRE( region.keyswitchRange == Range(0, 0) ); + } + + SECTION("sw_last") + { + REQUIRE( !region.keyswitch ); + region.parseOpcode({ "sw_last", "4" }); + REQUIRE( region.keyswitch ); + REQUIRE( *region.keyswitch == 4 ); + region.parseOpcode({ "sw_last", "128" }); + REQUIRE( region.keyswitch ); + REQUIRE( *region.keyswitch == 127 ); + region.parseOpcode({ "sw_last", "-1" }); + REQUIRE( region.keyswitch ); + REQUIRE( *region.keyswitch == 0 ); + } + + SECTION("sw_up") + { + REQUIRE( !region.keyswitchUp ); + region.parseOpcode({ "sw_up", "4" }); + REQUIRE( region.keyswitchUp ); + REQUIRE( *region.keyswitchUp == 4 ); + region.parseOpcode({ "sw_up", "128" }); + REQUIRE( region.keyswitchUp ); + REQUIRE( *region.keyswitchUp == 127 ); + region.parseOpcode({ "sw_up", "-1" }); + REQUIRE( region.keyswitchUp ); + REQUIRE( *region.keyswitchUp == 0 ); + } + + SECTION("sw_down") + { + REQUIRE( !region.keyswitchDown ); + region.parseOpcode({ "sw_down", "4" }); + REQUIRE( region.keyswitchDown ); + REQUIRE( *region.keyswitchDown == 4 ); + region.parseOpcode({ "sw_down", "128" }); + REQUIRE( region.keyswitchDown ); + REQUIRE( *region.keyswitchDown == 127 ); + region.parseOpcode({ "sw_down", "-1" }); + REQUIRE( region.keyswitchDown ); + REQUIRE( *region.keyswitchDown == 0 ); + } + + SECTION("sw_previous") + { + REQUIRE( !region.previousNote ); + region.parseOpcode({ "sw_previous", "4" }); + REQUIRE( region.previousNote ); + REQUIRE( *region.previousNote == 4 ); + region.parseOpcode({ "sw_previous", "128" }); + REQUIRE( region.previousNote ); + REQUIRE( *region.previousNote == 127 ); + region.parseOpcode({ "sw_previous", "-1" }); + REQUIRE( region.previousNote ); + REQUIRE( *region.previousNote == 0 ); + } + + SECTION("sw_vel") + { + REQUIRE( region.velocityOverride == SfzVelocityOverride::current ); + region.parseOpcode({ "sw_vel", "current" }); + REQUIRE( region.velocityOverride == SfzVelocityOverride::current ); + region.parseOpcode({ "sw_vel", "previous" }); + REQUIRE( region.velocityOverride == SfzVelocityOverride::previous ); + } + + SECTION("lochanaft, hichanaft") + { + REQUIRE( region.aftertouchRange == Range(0, 127) ); + region.parseOpcode({ "lochanaft", "4" }); + REQUIRE( region.aftertouchRange == Range(4, 127) ); + region.parseOpcode({ "lochanaft", "128" }); + REQUIRE( region.aftertouchRange == Range(127, 127) ); + region.parseOpcode({ "lochanaft", "0" }); + REQUIRE( region.aftertouchRange == Range(0, 127) ); + region.parseOpcode({ "hichanaft", "39" }); + REQUIRE( region.aftertouchRange == Range(0, 39) ); + region.parseOpcode({ "hichanaft", "135" }); + REQUIRE( region.aftertouchRange == Range(0, 127) ); + region.parseOpcode({ "hichanaft", "-1" }); + REQUIRE( region.aftertouchRange == Range(0, 0) ); + } + + SECTION("lobpm, hibpm") + { + REQUIRE( region.bpmRange == Range(0, 500) ); + region.parseOpcode({ "lobpm", "47.5" }); + REQUIRE( region.bpmRange == Range(47.5, 500) ); + region.parseOpcode({ "lobpm", "594" }); + REQUIRE( region.bpmRange == Range(500, 500) ); + region.parseOpcode({ "lobpm", "0" }); + REQUIRE( region.bpmRange == Range(0, 500) ); + region.parseOpcode({ "hibpm", "78" }); + REQUIRE( region.bpmRange == Range(0, 78) ); + region.parseOpcode({ "hibpm", "895.4" }); + REQUIRE( region.bpmRange == Range(0, 500) ); + region.parseOpcode({ "hibpm", "-1" }); + REQUIRE( region.bpmRange == Range(0, 0) ); + } + + SECTION("lorand, hirand") + { + REQUIRE( region.randRange == Range(0, 1) ); + region.parseOpcode({ "lorand", "0.5" }); + REQUIRE( region.randRange == Range(0.5, 1) ); + region.parseOpcode({ "lorand", "4" }); + REQUIRE( region.randRange == Range(1, 1) ); + region.parseOpcode({ "lorand", "0" }); + REQUIRE( region.randRange == Range(0, 1) ); + region.parseOpcode({ "hirand", "39" }); + REQUIRE( region.randRange == Range(0, 1) ); + region.parseOpcode({ "hirand", "0.7" }); + REQUIRE( region.randRange == Range(0, 0.7f) ); + region.parseOpcode({ "hirand", "-1" }); + REQUIRE( region.randRange == Range(0, 0) ); + } + + SECTION("seq_length") + { + REQUIRE( region.sequenceLength == 1 ); + region.parseOpcode({ "seq_length", "89" }); + REQUIRE( region.sequenceLength == 89 ); + region.parseOpcode({ "seq_length", "189" }); + REQUIRE( region.sequenceLength == 100 ); + region.parseOpcode({ "seq_length", "-1" }); + REQUIRE( region.sequenceLength == 1 ); + } + + SECTION("seq_position") + { + REQUIRE( region.sequencePosition == 1 ); + region.parseOpcode({ "seq_position", "89" }); + REQUIRE( region.sequencePosition == 89 ); + region.parseOpcode({ "seq_position", "189" }); + REQUIRE( region.sequencePosition == 100 ); + region.parseOpcode({ "seq_position", "-1" }); + REQUIRE( region.sequencePosition == 1 ); + } + + SECTION("trigger") + { + REQUIRE( region.trigger == SfzTrigger::attack ); + region.parseOpcode({ "trigger", "attack" }); + REQUIRE( region.trigger == SfzTrigger::attack ); + region.parseOpcode({ "trigger", "release" }); + REQUIRE( region.trigger == SfzTrigger::release ); + region.parseOpcode({ "trigger", "first" }); + REQUIRE( region.trigger == SfzTrigger::first ); + region.parseOpcode({ "trigger", "legato" }); + REQUIRE( region.trigger == SfzTrigger::legato ); + } + + SECTION("on_locc, on_hicc") + { + for (int ccIdx = 1; ccIdx < 128; ++ccIdx) + { + REQUIRE( !region.ccTriggers.contains(ccIdx) ); + } + region.parseOpcode({ "on_locc45", "15" }); + REQUIRE( region.ccTriggers.contains(45) ); + REQUIRE( region.ccTriggers[45] == Range(15, 127) ); + region.parseOpcode({ "on_hicc4", "47" }); + REQUIRE( region.ccTriggers.contains(45) ); + REQUIRE( region.ccTriggers[4] == Range(0, 47) ); + } + + SECTION("volume") + { + REQUIRE( region.volume == 0.0f ); + region.parseOpcode({ "volume", "4.2" }); + REQUIRE( region.volume == 4.2f ); + region.parseOpcode({ "volume", "-4.2" }); + REQUIRE( region.volume == -4.2f ); + region.parseOpcode({ "volume", "-123" }); + REQUIRE( region.volume == -123.0f ); + region.parseOpcode({ "volume", "-185" }); + REQUIRE( region.volume == -144.0f ); + region.parseOpcode({ "volume", "19" }); + REQUIRE( region.volume == 6.0f ); + } + + SECTION("pan") + { + REQUIRE( region.pan == 0.0f ); + region.parseOpcode({ "pan", "4.2" }); + REQUIRE( region.pan == 4.2f ); + region.parseOpcode({ "pan", "-4.2" }); + REQUIRE( region.pan == -4.2f ); + region.parseOpcode({ "pan", "-123" }); + REQUIRE( region.pan == -100.0f ); + region.parseOpcode({ "pan", "132" }); + REQUIRE( region.pan == 100.0f ); + } + + SECTION("pan_oncc") + { + REQUIRE( !region.panCC ); + region.parseOpcode({ "pan_oncc45", "4.2" }); + REQUIRE( region.panCC ); + REQUIRE( region.panCC->first == 45 ); + REQUIRE( region.panCC->second == 4.2f ); + } + + SECTION("width") + { + REQUIRE( region.width == 0.0f ); + region.parseOpcode({ "width", "4.2" }); + REQUIRE( region.width == 4.2f ); + region.parseOpcode({ "width", "-4.2" }); + REQUIRE( region.width == -4.2f ); + region.parseOpcode({ "width", "-123" }); + REQUIRE( region.width == -100.0f ); + region.parseOpcode({ "width", "132" }); + REQUIRE( region.width == 100.0f ); + } + + SECTION("width_oncc") + { + REQUIRE( !region.widthCC ); + region.parseOpcode({ "width_oncc45", "4.2" }); + REQUIRE( region.widthCC ); + REQUIRE( region.widthCC->first == 45 ); + REQUIRE( region.widthCC->second == 4.2f ); + } + + SECTION("position") + { + REQUIRE( region.position == 0.0f ); + region.parseOpcode({ "position", "4.2" }); + REQUIRE( region.position == 4.2f ); + region.parseOpcode({ "position", "-4.2" }); + REQUIRE( region.position == -4.2f ); + region.parseOpcode({ "position", "-123" }); + REQUIRE( region.position == -100.0f ); + region.parseOpcode({ "position", "132" }); + REQUIRE( region.position == 100.0f ); + } + + SECTION("position_oncc") + { + REQUIRE( !region.positionCC ); + region.parseOpcode({ "position_oncc45", "4.2" }); + REQUIRE( region.positionCC ); + REQUIRE( region.positionCC->first == 45 ); + REQUIRE( region.positionCC->second == 4.2f ); + } + + SECTION("amp_keycenter") + { + REQUIRE( region.ampKeycenter == 60 ); + region.parseOpcode({ "amp_keycenter", "40" }); + REQUIRE( region.ampKeycenter == 40 ); + region.parseOpcode({ "amp_keycenter", "-1" }); + REQUIRE( region.ampKeycenter == 0 ); + region.parseOpcode({ "amp_keycenter", "132" }); + REQUIRE( region.ampKeycenter == 127 ); + } + + SECTION("amp_keytrack") + { + REQUIRE( region.ampKeytrack == 0.0f ); + region.parseOpcode({ "amp_keytrack", "4.2" }); + REQUIRE( region.ampKeytrack == 4.2f ); + region.parseOpcode({ "amp_keytrack", "-4.2" }); + REQUIRE( region.ampKeytrack == -4.2f ); + region.parseOpcode({ "amp_keytrack", "-123" }); + REQUIRE( region.ampKeytrack == -96.0f ); + region.parseOpcode({ "amp_keytrack", "132" }); + REQUIRE( region.ampKeytrack == 12.0f ); + } + + SECTION("amp_veltrack") + { + REQUIRE( region.ampVeltrack == 100.0f ); + region.parseOpcode({ "amp_veltrack", "4.2" }); + REQUIRE( region.ampVeltrack == 4.2f ); + region.parseOpcode({ "amp_veltrack", "-4.2" }); + REQUIRE( region.ampVeltrack == -4.2f ); + region.parseOpcode({ "amp_veltrack", "-123" }); + REQUIRE( region.ampVeltrack == -100.0f ); + region.parseOpcode({ "amp_veltrack", "132" }); + REQUIRE( region.ampVeltrack == 100.0f ); + } + + SECTION("amp_random") + { + REQUIRE( region.ampRandom == 0.0f ); + region.parseOpcode({ "amp_random", "4.2" }); + REQUIRE( region.ampRandom == 4.2f ); + region.parseOpcode({ "amp_random", "-4.2" }); + REQUIRE( region.ampRandom == 0.0f ); + region.parseOpcode({ "amp_random", "132" }); + REQUIRE( region.ampRandom == 24.0f ); + } + + SECTION("amp_velcurve") + { + region.parseOpcode({ "amp_velcurve_6", "0.4" }); + REQUIRE( region.velocityPoints.back() == std::make_pair(6, 0.4f) ); + region.parseOpcode({ "amp_velcurve_127", "-1.0" }); + REQUIRE( region.velocityPoints.back() == std::make_pair(127, 0.0f) ); + } + + SECTION("xfin_lokey, xfin_hikey") + { + REQUIRE(region.crossfadeKeyInRange == Range(0, 0)); + region.parseOpcode({"xfin_lokey", "4"}); + REQUIRE(region.crossfadeKeyInRange == Range(4, 4)); + region.parseOpcode({"xfin_lokey", "128"}); + REQUIRE(region.crossfadeKeyInRange == Range(127, 127)); + region.parseOpcode({"xfin_lokey", "59"}); + REQUIRE(region.crossfadeKeyInRange == Range(59, 127)); + region.parseOpcode({"xfin_hikey", "59"}); + REQUIRE(region.crossfadeKeyInRange == Range(59, 59)); + region.parseOpcode({"xfin_hikey", "128"}); + REQUIRE(region.crossfadeKeyInRange == Range(59, 127)); + region.parseOpcode({"xfin_hikey", "0"}); + REQUIRE(region.crossfadeKeyInRange == Range(0, 0)); + region.parseOpcode({"xfin_hikey", "-1"}); + REQUIRE(region.crossfadeKeyInRange == Range(0, 0)); + } + + SECTION("xfin_lovel, xfin_hivel") + { + REQUIRE(region.crossfadeVelInRange == Range(0, 0)); + region.parseOpcode({"xfin_lovel", "4"}); + REQUIRE(region.crossfadeVelInRange == Range(4, 4)); + region.parseOpcode({"xfin_lovel", "128"}); + REQUIRE(region.crossfadeVelInRange == Range(127, 127)); + region.parseOpcode({"xfin_lovel", "59"}); + REQUIRE(region.crossfadeVelInRange == Range(59, 127)); + region.parseOpcode({"xfin_hivel", "59"}); + REQUIRE(region.crossfadeVelInRange == Range(59, 59)); + region.parseOpcode({"xfin_hivel", "128"}); + REQUIRE(region.crossfadeVelInRange == Range(59, 127)); + region.parseOpcode({"xfin_hivel", "0"}); + REQUIRE(region.crossfadeVelInRange == Range(0, 0)); + region.parseOpcode({"xfin_hivel", "-1"}); + REQUIRE(region.crossfadeVelInRange == Range(0, 0)); + } + + SECTION("xfout_lokey, xfout_hikey") + { + REQUIRE(region.crossfadeKeyOutRange == Range(127, 127)); + region.parseOpcode({"xfout_lokey", "4"}); + REQUIRE(region.crossfadeKeyOutRange == Range(4, 127)); + region.parseOpcode({"xfout_lokey", "128"}); + REQUIRE(region.crossfadeKeyOutRange == Range(127, 127)); + region.parseOpcode({"xfout_lokey", "59"}); + REQUIRE(region.crossfadeKeyOutRange == Range(59, 127)); + region.parseOpcode({"xfout_hikey", "59"}); + REQUIRE(region.crossfadeKeyOutRange == Range(59, 59)); + region.parseOpcode({"xfout_hikey", "128"}); + REQUIRE(region.crossfadeKeyOutRange == Range(59, 127)); + region.parseOpcode({"xfout_hikey", "0"}); + REQUIRE(region.crossfadeKeyOutRange == Range(0, 0)); + region.parseOpcode({"xfout_hikey", "-1"}); + REQUIRE(region.crossfadeKeyOutRange == Range(0, 0)); + } + + SECTION("xfout_lovel, xfout_hivel") + { + REQUIRE(region.crossfadeVelOutRange == Range(127, 127)); + region.parseOpcode({"xfout_lovel", "4"}); + REQUIRE(region.crossfadeVelOutRange == Range(4, 127)); + region.parseOpcode({"xfout_lovel", "128"}); + REQUIRE(region.crossfadeVelOutRange == Range(127, 127)); + region.parseOpcode({"xfout_lovel", "59"}); + REQUIRE(region.crossfadeVelOutRange == Range(59, 127)); + region.parseOpcode({"xfout_hivel", "59"}); + REQUIRE(region.crossfadeVelOutRange == Range(59, 59)); + region.parseOpcode({"xfout_hivel", "128"}); + REQUIRE(region.crossfadeVelOutRange == Range(59, 127)); + region.parseOpcode({"xfout_hivel", "0"}); + REQUIRE(region.crossfadeVelOutRange == Range(0, 0)); + region.parseOpcode({"xfout_hivel", "-1"}); + REQUIRE(region.crossfadeVelOutRange == Range(0, 0)); + } + + SECTION("xf_keycurve") + { + REQUIRE(region.crossfadeKeyCurve == SfzCrossfadeCurve::power); + region.parseOpcode({"xf_keycurve", "gain"}); + REQUIRE(region.crossfadeKeyCurve == SfzCrossfadeCurve::gain); + region.parseOpcode({"xf_keycurve", "power"}); + REQUIRE(region.crossfadeKeyCurve == SfzCrossfadeCurve::power); + region.parseOpcode({"xf_keycurve", "something"}); + REQUIRE(region.crossfadeKeyCurve == SfzCrossfadeCurve::power); + region.parseOpcode({"xf_keycurve", "gain"}); + region.parseOpcode({"xf_keycurve", "something"}); + REQUIRE(region.crossfadeKeyCurve == SfzCrossfadeCurve::gain); + } + + SECTION("xf_velcurve") + { + REQUIRE(region.crossfadeVelCurve == SfzCrossfadeCurve::power); + region.parseOpcode({"xf_velcurve", "gain"}); + REQUIRE(region.crossfadeVelCurve == SfzCrossfadeCurve::gain); + region.parseOpcode({"xf_velcurve", "power"}); + REQUIRE(region.crossfadeVelCurve == SfzCrossfadeCurve::power); + region.parseOpcode({"xf_velcurve", "something"}); + REQUIRE(region.crossfadeVelCurve == SfzCrossfadeCurve::power); + region.parseOpcode({"xf_velcurve", "gain"}); + region.parseOpcode({"xf_velcurve", "something"}); + REQUIRE(region.crossfadeVelCurve == SfzCrossfadeCurve::gain); + } + + SECTION("pitch_keycenter") + { + REQUIRE( region.pitchKeycenter == 60 ); + region.parseOpcode({ "pitch_keycenter", "40" }); + REQUIRE( region.pitchKeycenter == 40 ); + region.parseOpcode({ "pitch_keycenter", "-1" }); + REQUIRE( region.pitchKeycenter == 0 ); + region.parseOpcode({ "pitch_keycenter", "132" }); + REQUIRE( region.pitchKeycenter == 127 ); + } + + SECTION("pitch_keytrack") + { + REQUIRE( region.pitchKeytrack == 100 ); + region.parseOpcode({ "pitch_keytrack", "40" }); + REQUIRE( region.pitchKeytrack == 40 ); + region.parseOpcode({ "pitch_keytrack", "-1" }); + REQUIRE( region.pitchKeytrack == -1 ); + region.parseOpcode({ "pitch_keytrack", "1320" }); + REQUIRE( region.pitchKeytrack == 1200 ); + region.parseOpcode({ "pitch_keytrack", "-1320" }); + REQUIRE( region.pitchKeytrack == -1200 ); + } + + SECTION("pitch_random") + { + REQUIRE( region.pitchRandom == 0 ); + region.parseOpcode({ "pitch_random", "40" }); + REQUIRE( region.pitchRandom == 40 ); + region.parseOpcode({ "pitch_random", "-1" }); + REQUIRE( region.pitchRandom == 0 ); + region.parseOpcode({ "pitch_random", "10320" }); + REQUIRE( region.pitchRandom == 9600 ); + } + + SECTION("pitch_veltrack") + { + REQUIRE( region.pitchVeltrack == 0 ); + region.parseOpcode({ "pitch_veltrack", "40" }); + REQUIRE( region.pitchVeltrack == 40 ); + region.parseOpcode({ "pitch_veltrack", "-1" }); + REQUIRE( region.pitchVeltrack == -1 ); + region.parseOpcode({ "pitch_veltrack", "13020" }); + REQUIRE( region.pitchVeltrack == 9600 ); + region.parseOpcode({ "pitch_veltrack", "-13020" }); + REQUIRE( region.pitchVeltrack == -9600 ); + } + + SECTION("transpose") + { + REQUIRE( region.transpose == 0 ); + region.parseOpcode({ "transpose", "40" }); + REQUIRE( region.transpose == 40 ); + region.parseOpcode({ "transpose", "-1" }); + REQUIRE( region.transpose == -1 ); + region.parseOpcode({ "transpose", "154" }); + REQUIRE( region.transpose == 127 ); + region.parseOpcode({ "transpose", "-154" }); + REQUIRE( region.transpose == -127 ); + } + + SECTION("tune") + { + REQUIRE( region.tune == 0 ); + region.parseOpcode({ "tune", "40" }); + REQUIRE( region.tune == 40 ); + region.parseOpcode({ "tune", "-1" }); + REQUIRE( region.tune == -1 ); + region.parseOpcode({ "tune", "154" }); + REQUIRE( region.tune == 100 ); + region.parseOpcode({ "tune", "-154" }); + REQUIRE( region.tune == -100 ); + } + + SECTION("ampeg") + { + // Defaults + REQUIRE( region.amplitudeEG.attack == 0.0f ); + 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.start == 0.0f ); + REQUIRE( region.amplitudeEG.sustain == 100.0f ); + REQUIRE( region.amplitudeEG.depth == 0 ); + REQUIRE( region.amplitudeEG.vel2attack == 0.0f ); + REQUIRE( region.amplitudeEG.vel2decay == 0.0f ); + REQUIRE( region.amplitudeEG.vel2delay == 0.0f ); + REQUIRE( region.amplitudeEG.vel2hold == 0.0f ); + REQUIRE( region.amplitudeEG.vel2release == 0.0f ); + REQUIRE( region.amplitudeEG.vel2sustain == 0.0f ); + REQUIRE( region.amplitudeEG.vel2depth == 0 ); + // + region.parseOpcode({ "ampeg_attack", "1" }); + region.parseOpcode({ "ampeg_decay", "2" }); + region.parseOpcode({ "ampeg_delay", "3" }); + region.parseOpcode({ "ampeg_hold", "4" }); + region.parseOpcode({ "ampeg_release", "5" }); + region.parseOpcode({ "ampeg_start", "6" }); + region.parseOpcode({ "ampeg_sustain", "7" }); + region.parseOpcode({ "ampeg_depth", "8" }); + region.parseOpcode({ "ampeg_vel2attack", "9" }); + region.parseOpcode({ "ampeg_vel2decay", "10" }); + region.parseOpcode({ "ampeg_vel2delay", "11" }); + region.parseOpcode({ "ampeg_vel2hold", "12" }); + region.parseOpcode({ "ampeg_vel2release", "13" }); + region.parseOpcode({ "ampeg_vel2sustain", "14" }); + region.parseOpcode({ "ampeg_vel2depth", "15" }); + REQUIRE( region.amplitudeEG.attack == 1.0f ); + REQUIRE( region.amplitudeEG.decay == 2.0f ); + REQUIRE( region.amplitudeEG.delay == 3.0f ); + REQUIRE( region.amplitudeEG.hold == 4.0f ); + REQUIRE( region.amplitudeEG.release == 5.0f ); + REQUIRE( region.amplitudeEG.start == 6.0f ); + REQUIRE( region.amplitudeEG.sustain == 7.0f ); + REQUIRE( region.amplitudeEG.depth == 0 ); // ignored for ampeg + REQUIRE( region.amplitudeEG.vel2attack == 9.0f ); + REQUIRE( region.amplitudeEG.vel2decay == 10.0f ); + REQUIRE( region.amplitudeEG.vel2delay == 11.0f ); + REQUIRE( region.amplitudeEG.vel2hold == 12.0f ); + REQUIRE( region.amplitudeEG.vel2release == 13.0f ); + REQUIRE( region.amplitudeEG.vel2sustain == 14.0f ); + REQUIRE( region.amplitudeEG.vel2depth == 0 ); // ignored for ampeg + // + region.parseOpcode({ "ampeg_attack", "1000" }); + region.parseOpcode({ "ampeg_decay", "1000" }); + region.parseOpcode({ "ampeg_delay", "1000" }); + region.parseOpcode({ "ampeg_hold", "1000" }); + region.parseOpcode({ "ampeg_release", "1000" }); + region.parseOpcode({ "ampeg_start", "1000" }); + region.parseOpcode({ "ampeg_sustain", "1000" }); + region.parseOpcode({ "ampeg_depth", "1000" }); + region.parseOpcode({ "ampeg_vel2attack", "1000" }); + region.parseOpcode({ "ampeg_vel2decay", "1000" }); + region.parseOpcode({ "ampeg_vel2delay", "1000" }); + region.parseOpcode({ "ampeg_vel2hold", "1000" }); + region.parseOpcode({ "ampeg_vel2release", "1000" }); + region.parseOpcode({ "ampeg_vel2sustain", "1000" }); + region.parseOpcode({ "ampeg_vel2depth", "1000" }); + REQUIRE( region.amplitudeEG.attack == 100.0f ); + REQUIRE( region.amplitudeEG.decay == 100.0f ); + REQUIRE( region.amplitudeEG.delay == 100.0f ); + REQUIRE( region.amplitudeEG.hold == 100.0f ); + REQUIRE( region.amplitudeEG.release == 100.0f ); + REQUIRE( region.amplitudeEG.start == 100.0f ); + REQUIRE( region.amplitudeEG.sustain == 100.0f ); + REQUIRE( region.amplitudeEG.depth == 0 ); // ignored for ampeg + REQUIRE( region.amplitudeEG.vel2attack == 100.0f ); + REQUIRE( region.amplitudeEG.vel2decay == 100.0f ); + REQUIRE( region.amplitudeEG.vel2delay == 100.0f ); + REQUIRE( region.amplitudeEG.vel2hold == 100.0f ); + REQUIRE( region.amplitudeEG.vel2release == 100.0f ); + REQUIRE( region.amplitudeEG.vel2sustain == 100.0f ); + REQUIRE( region.amplitudeEG.vel2depth == 0 ); // ignored for ampeg + // + region.parseOpcode({ "ampeg_attack", "-101" }); + region.parseOpcode({ "ampeg_decay", "-101" }); + region.parseOpcode({ "ampeg_delay", "-101" }); + region.parseOpcode({ "ampeg_hold", "-101" }); + region.parseOpcode({ "ampeg_release", "-101" }); + region.parseOpcode({ "ampeg_start", "-101" }); + region.parseOpcode({ "ampeg_sustain", "-101" }); + region.parseOpcode({ "ampeg_depth", "-101" }); + region.parseOpcode({ "ampeg_vel2attack", "-101" }); + region.parseOpcode({ "ampeg_vel2decay", "-101" }); + region.parseOpcode({ "ampeg_vel2delay", "-101" }); + region.parseOpcode({ "ampeg_vel2hold", "-101" }); + region.parseOpcode({ "ampeg_vel2release", "-101" }); + region.parseOpcode({ "ampeg_vel2sustain", "-101" }); + region.parseOpcode({ "ampeg_vel2depth", "-101" }); + REQUIRE( region.amplitudeEG.attack == 0.0f ); + 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.start == 0.0f ); + REQUIRE( region.amplitudeEG.sustain == 0.0f ); + REQUIRE( region.amplitudeEG.depth == 0 ); // ignored for ampeg + REQUIRE( region.amplitudeEG.vel2attack == -100.0f ); + REQUIRE( region.amplitudeEG.vel2decay == -100.0f ); + REQUIRE( region.amplitudeEG.vel2delay == -100.0f ); + REQUIRE( region.amplitudeEG.vel2hold == -100.0f ); + REQUIRE( region.amplitudeEG.vel2release == -100.0f ); + REQUIRE( region.amplitudeEG.vel2sustain == -100.0f ); + } + + SECTION("ampeg_XX_onccNN") + { + // Defaults + REQUIRE( !region.amplitudeEG.ccAttack ); + REQUIRE( !region.amplitudeEG.ccDecay ); + REQUIRE( !region.amplitudeEG.ccDelay ); + REQUIRE( !region.amplitudeEG.ccHold ); + REQUIRE( !region.amplitudeEG.ccRelease ); + REQUIRE( !region.amplitudeEG.ccStart ); + REQUIRE( !region.amplitudeEG.ccSustain ); + // + region.parseOpcode({ "ampeg_attack_oncc1", "1" }); + region.parseOpcode({ "ampeg_decay_oncc2", "2" }); + region.parseOpcode({ "ampeg_delay_oncc3", "3" }); + region.parseOpcode({ "ampeg_hold_oncc4", "4" }); + region.parseOpcode({ "ampeg_release_oncc5", "5" }); + region.parseOpcode({ "ampeg_start_oncc6", "6" }); + region.parseOpcode({ "ampeg_sustain_oncc7", "7" }); + REQUIRE( region.amplitudeEG.ccAttack ); + REQUIRE( region.amplitudeEG.ccDecay ); + REQUIRE( region.amplitudeEG.ccDelay ); + REQUIRE( region.amplitudeEG.ccHold ); + REQUIRE( region.amplitudeEG.ccRelease ); + REQUIRE( region.amplitudeEG.ccStart ); + REQUIRE( region.amplitudeEG.ccSustain ); + REQUIRE( region.amplitudeEG.ccAttack->first == 1 ); + REQUIRE( region.amplitudeEG.ccDecay->first == 2 ); + REQUIRE( region.amplitudeEG.ccDelay->first == 3 ); + REQUIRE( region.amplitudeEG.ccHold->first == 4 ); + REQUIRE( region.amplitudeEG.ccRelease->first == 5 ); + REQUIRE( region.amplitudeEG.ccStart->first == 6 ); + REQUIRE( region.amplitudeEG.ccSustain->first == 7 ); + REQUIRE( region.amplitudeEG.ccAttack->second == 1.0f ); + REQUIRE( region.amplitudeEG.ccDecay->second == 2.0f ); + REQUIRE( region.amplitudeEG.ccDelay->second == 3.0f ); + REQUIRE( region.amplitudeEG.ccHold->second == 4.0f ); + REQUIRE( region.amplitudeEG.ccRelease->second == 5.0f ); + REQUIRE( region.amplitudeEG.ccStart->second == 6.0f ); + REQUIRE( region.amplitudeEG.ccSustain->second == 7.0f ); + // + region.parseOpcode({ "ampeg_attack_oncc1", "101" }); + region.parseOpcode({ "ampeg_decay_oncc2", "101" }); + region.parseOpcode({ "ampeg_delay_oncc3", "101" }); + region.parseOpcode({ "ampeg_hold_oncc4", "101" }); + region.parseOpcode({ "ampeg_release_oncc5", "101" }); + region.parseOpcode({ "ampeg_start_oncc6", "101" }); + region.parseOpcode({ "ampeg_sustain_oncc7", "101" }); + REQUIRE( region.amplitudeEG.ccAttack->second == 100.0f ); + REQUIRE( region.amplitudeEG.ccDecay->second == 100.0f ); + REQUIRE( region.amplitudeEG.ccDelay->second == 100.0f ); + REQUIRE( region.amplitudeEG.ccHold->second == 100.0f ); + REQUIRE( region.amplitudeEG.ccRelease->second == 100.0f ); + REQUIRE( region.amplitudeEG.ccStart->second == 100.0f ); + REQUIRE( region.amplitudeEG.ccSustain->second == 100.0f ); + // + region.parseOpcode({ "ampeg_attack_oncc1", "-101" }); + region.parseOpcode({ "ampeg_decay_oncc2", "-101" }); + region.parseOpcode({ "ampeg_delay_oncc3", "-101" }); + region.parseOpcode({ "ampeg_hold_oncc4", "-101" }); + region.parseOpcode({ "ampeg_release_oncc5", "-101" }); + region.parseOpcode({ "ampeg_start_oncc6", "-101" }); + region.parseOpcode({ "ampeg_sustain_oncc7", "-101" }); + REQUIRE( region.amplitudeEG.ccAttack->second == -100.0f ); + REQUIRE( region.amplitudeEG.ccDecay->second == -100.0f ); + REQUIRE( region.amplitudeEG.ccDelay->second == -100.0f ); + REQUIRE( region.amplitudeEG.ccHold->second == -100.0f ); + REQUIRE( region.amplitudeEG.ccRelease->second == -100.0f ); + REQUIRE( region.amplitudeEG.ccStart->second == -100.0f ); + REQUIRE( region.amplitudeEG.ccSustain->second == -100.0f ); + } +} \ No newline at end of file diff --git a/tests/TestFiles/Includes/included.sfz b/tests/TestFiles/Includes/included.sfz new file mode 100644 index 00000000..665062c0 --- /dev/null +++ b/tests/TestFiles/Includes/included.sfz @@ -0,0 +1 @@ + sample=dummy.wav \ No newline at end of file diff --git a/tests/TestFiles/Includes/included_2.sfz b/tests/TestFiles/Includes/included_2.sfz new file mode 100644 index 00000000..c48ab980 --- /dev/null +++ b/tests/TestFiles/Includes/included_2.sfz @@ -0,0 +1 @@ + sample=dummy2.wav \ No newline at end of file diff --git a/tests/TestFiles/Includes/included_loop1.sfz b/tests/TestFiles/Includes/included_loop1.sfz new file mode 100644 index 00000000..cef331a6 --- /dev/null +++ b/tests/TestFiles/Includes/included_loop1.sfz @@ -0,0 +1,2 @@ +#include "included_loop2.sfz" + sample=dummy_loop1.wav \ No newline at end of file diff --git a/tests/TestFiles/Includes/included_loop2.sfz b/tests/TestFiles/Includes/included_loop2.sfz new file mode 100644 index 00000000..c66e5cdd --- /dev/null +++ b/tests/TestFiles/Includes/included_loop2.sfz @@ -0,0 +1,3 @@ +#include "included_loop1.sfz" +#include "root_loop.sfz" + sample=dummy_loop2.wav \ No newline at end of file diff --git a/tests/TestFiles/Includes/included_recursive1.sfz b/tests/TestFiles/Includes/included_recursive1.sfz new file mode 100644 index 00000000..473da8af --- /dev/null +++ b/tests/TestFiles/Includes/included_recursive1.sfz @@ -0,0 +1,2 @@ +#include "included_recursive2.sfz" + sample=dummy_recursive1.wav \ No newline at end of file diff --git a/tests/TestFiles/Includes/included_recursive2.sfz b/tests/TestFiles/Includes/included_recursive2.sfz new file mode 100644 index 00000000..25be8a90 --- /dev/null +++ b/tests/TestFiles/Includes/included_recursive2.sfz @@ -0,0 +1 @@ + sample=dummy_recursive2.wav \ No newline at end of file diff --git a/tests/TestFiles/Includes/multiple_includes.sfz b/tests/TestFiles/Includes/multiple_includes.sfz new file mode 100644 index 00000000..3ccc2ebf --- /dev/null +++ b/tests/TestFiles/Includes/multiple_includes.sfz @@ -0,0 +1,2 @@ +#include "included.sfz" +#include "included_2.sfz" \ No newline at end of file diff --git a/tests/TestFiles/Includes/multiple_includes_with_comments.sfz b/tests/TestFiles/Includes/multiple_includes_with_comments.sfz new file mode 100644 index 00000000..26399a05 --- /dev/null +++ b/tests/TestFiles/Includes/multiple_includes_with_comments.sfz @@ -0,0 +1,6 @@ +//DUMMY comment + +#include "included.sfz" +#include "included_2.sfz" + +//EOF \ No newline at end of file diff --git a/tests/TestFiles/Includes/root_local.sfz b/tests/TestFiles/Includes/root_local.sfz new file mode 100644 index 00000000..f3bd9e9a --- /dev/null +++ b/tests/TestFiles/Includes/root_local.sfz @@ -0,0 +1 @@ +#include "included.sfz" \ No newline at end of file diff --git a/tests/TestFiles/Includes/root_loop.sfz b/tests/TestFiles/Includes/root_loop.sfz new file mode 100644 index 00000000..58d7d45b --- /dev/null +++ b/tests/TestFiles/Includes/root_loop.sfz @@ -0,0 +1 @@ +#include "included_loop1.sfz" \ No newline at end of file diff --git a/tests/TestFiles/Includes/root_recursive.sfz b/tests/TestFiles/Includes/root_recursive.sfz new file mode 100644 index 00000000..db3e9fc0 --- /dev/null +++ b/tests/TestFiles/Includes/root_recursive.sfz @@ -0,0 +1 @@ +#include "included_recursive1.sfz" \ No newline at end of file diff --git a/tests/TestFiles/Includes/root_subdir.sfz b/tests/TestFiles/Includes/root_subdir.sfz new file mode 100644 index 00000000..9a19ddf8 --- /dev/null +++ b/tests/TestFiles/Includes/root_subdir.sfz @@ -0,0 +1 @@ +#include "subdir/included_subdir.sfz" \ No newline at end of file diff --git a/tests/TestFiles/Includes/root_subdir_win.sfz b/tests/TestFiles/Includes/root_subdir_win.sfz new file mode 100644 index 00000000..a3cdbf51 --- /dev/null +++ b/tests/TestFiles/Includes/root_subdir_win.sfz @@ -0,0 +1 @@ +#include "subdir\included_subdir.sfz" \ No newline at end of file diff --git a/tests/TestFiles/Includes/subdir/included_subdir.sfz b/tests/TestFiles/Includes/subdir/included_subdir.sfz new file mode 100644 index 00000000..c96e776b --- /dev/null +++ b/tests/TestFiles/Includes/subdir/included_subdir.sfz @@ -0,0 +1 @@ + sample=dummy_subdir.wav \ No newline at end of file diff --git a/tests/TestFiles/Regions/dummy.1.wav b/tests/TestFiles/Regions/dummy.1.wav new file mode 100644 index 00000000..e69de29b diff --git a/tests/TestFiles/Regions/dummy.2.wav b/tests/TestFiles/Regions/dummy.2.wav new file mode 100644 index 00000000..e69de29b diff --git a/tests/TestFiles/Regions/dummy.wav b/tests/TestFiles/Regions/dummy.wav new file mode 100644 index 00000000..e69de29b diff --git a/tests/TestFiles/Regions/regions_many.sfz b/tests/TestFiles/Regions/regions_many.sfz new file mode 100644 index 00000000..199c052b --- /dev/null +++ b/tests/TestFiles/Regions/regions_many.sfz @@ -0,0 +1,3 @@ + sample=dummy.wav + sample=dummy.1.wav + sample=dummy.2.wav \ No newline at end of file diff --git a/tests/TestFiles/Regions/regions_one.sfz b/tests/TestFiles/Regions/regions_one.sfz new file mode 100644 index 00000000..665062c0 --- /dev/null +++ b/tests/TestFiles/Regions/regions_one.sfz @@ -0,0 +1 @@ + sample=dummy.wav \ No newline at end of file diff --git a/tests/TestFiles/Regions/regions_opcodes.sfz b/tests/TestFiles/Regions/regions_opcodes.sfz new file mode 100644 index 00000000..a301981b --- /dev/null +++ b/tests/TestFiles/Regions/regions_opcodes.sfz @@ -0,0 +1 @@ + lochan=2 hichan=14 sample=dummy.wav \ No newline at end of file diff --git a/tests/TestFiles/Regions/underscore_opcodes.sfz b/tests/TestFiles/Regions/underscore_opcodes.sfz new file mode 100644 index 00000000..5b9a1455 --- /dev/null +++ b/tests/TestFiles/Regions/underscore_opcodes.sfz @@ -0,0 +1 @@ + loop_mode=loop_sustain sample=dummy.wav \ No newline at end of file diff --git a/tests/TestFiles/SpecificBugs/MeatBassPizz/Programs/pizz.sfz b/tests/TestFiles/SpecificBugs/MeatBassPizz/Programs/pizz.sfz new file mode 100644 index 00000000..2ddb1515 --- /dev/null +++ b/tests/TestFiles/SpecificBugs/MeatBassPizz/Programs/pizz.sfz @@ -0,0 +1,24 @@ + +lokey=12 +hikey=22 +lovel=97 +pitch_keycenter=21 +hicc107=13 + + +sample=..\Samples\pizz\a0_vl4_rr1.wav +hirand=0.250 + + +sample=..\Samples\pizz\a0_vl4_rr2.wav +lorand=0.250 +hirand=0.500 + + +sample=..\Samples\pizz\a0_vl4_rr3.wav +lorand=0.500 +hirand=0.750 + + +sample=..\Samples\pizz\a0_vl4_rr4.wav +lorand=0.750 \ No newline at end of file diff --git a/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr1.wav b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr1.wav new file mode 100644 index 00000000..680959f0 Binary files /dev/null and b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr1.wav differ diff --git a/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr2.wav b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr2.wav new file mode 100644 index 00000000..8a536163 Binary files /dev/null and b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr2.wav differ diff --git a/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr3.wav b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr3.wav new file mode 100644 index 00000000..88b7d818 Binary files /dev/null and b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr3.wav differ diff --git a/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr4.wav b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr4.wav new file mode 100644 index 00000000..32a2439f Binary files /dev/null and b/tests/TestFiles/SpecificBugs/MeatBassPizz/Samples/pizz/a0_vl4_rr4.wav differ diff --git a/tests/TestFiles/basic_hierarchy.sfz b/tests/TestFiles/basic_hierarchy.sfz new file mode 100644 index 00000000..f6449ed8 --- /dev/null +++ b/tests/TestFiles/basic_hierarchy.sfz @@ -0,0 +1,25 @@ + width=40 + + pan=30 + + +delay=67 + key=60 sample=Regions/dummy.wav + key=61 sample=Regions/dummy.1.wav + + +delay=56 + key=50 sample=Regions/dummy.wav + key=51 sample=Regions/dummy.1.wav + + pan=-10 + + +delay=47 + key=40 sample=Regions/dummy.wav + key=41 sample=Regions/dummy.1.wav + + +delay=36 + key=30 sample=Regions/dummy.wav + key=31 sample=Regions/dummy.1.wav \ No newline at end of file diff --git a/tests/TestFiles/basic_hierarchy_antislash.sfz b/tests/TestFiles/basic_hierarchy_antislash.sfz new file mode 100644 index 00000000..c1647012 --- /dev/null +++ b/tests/TestFiles/basic_hierarchy_antislash.sfz @@ -0,0 +1,25 @@ + width=40 + + pan=30 + + +delay=67 + key=60 sample=Regions\dummy.wav + key=61 sample=Regions\dummy.1.wav + + +delay=56 + key=50 sample=Regions\dummy.wav + key=51 sample=Regions\dummy.1.wav + + pan=-10 + + +delay=47 + key=40 sample=Regions\dummy.wav + key=41 sample=Regions\dummy.1.wav + + +delay=36 + key=30 sample=Regions\dummy.wav + key=31 sample=Regions\dummy.1.wav \ No newline at end of file diff --git a/tests/TestFiles/defines.sfz b/tests/TestFiles/defines.sfz new file mode 100644 index 00000000..81e5aa56 --- /dev/null +++ b/tests/TestFiles/defines.sfz @@ -0,0 +1,8 @@ + +#define $KICKKEY 36 +#define $SNAREKEY 38 +#define $HATKEY 42 + +key=$KICKKEY sample=kick.wav +key=$SNAREKEY sample=snare.wav +key=$HATKEY sample=closedhat.wav \ No newline at end of file diff --git a/tests/TestFiles/groups_avl.sfz b/tests/TestFiles/groups_avl.sfz new file mode 100644 index 00000000..65cae609 --- /dev/null +++ b/tests/TestFiles/groups_avl.sfz @@ -0,0 +1,6 @@ + key=36 volume=6 + lovel=1 hivel=26 sample=36-CajonCenter-1.wav + lovel=27 hivel=52 sample=36-CajonCenter-2.wav + lovel=53 hivel=77 sample=36-CajonCenter-3.wav + lovel=78 hivel=102 sample=36-CajonCenter-4.wav + lovel=103 hivel=127 sample=36-CajonCenter-5.wav \ No newline at end of file diff --git a/tests/TestFiles/sw_default.sfz b/tests/TestFiles/sw_default.sfz new file mode 100644 index 00000000..44d896c5 --- /dev/null +++ b/tests/TestFiles/sw_default.sfz @@ -0,0 +1,5 @@ + sw_lokey=30 sw_hikey=50 sw_default=40 + sw_last=41 key=51 sample=*silence + sw_last=40 key=52 sample=*silence + sw_last=41 key=53 sample=*silence + sw_last=40 key=54 sample=*silence \ No newline at end of file