// SPDX-License-Identifier: BSD-2-Clause // This code is part of the sfizz library and is licensed under a BSD 2-clause // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "TestHelpers.h" #include "sfizz/Synth.h" #include "sfizz/Voice.h" #include "sfizz/SfzHelpers.h" #include "sfizz/parser/Parser.h" #include "sfizz/modulations/ModId.h" #include "sfizz/modulations/ModKey.h" #include "catch2/catch.hpp" #include "ghc/fs_std.hpp" #if defined(__APPLE__) #include // pathconf #endif using namespace Catch::literals; using namespace sfz::literals; using namespace sfz; TEST_CASE("[Files] Single region (regions_one.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Regions/regions_one.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy.wav"); } TEST_CASE("[Files] Multiple regions (regions_many.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Regions/regions_many.sfz"); REQUIRE(synth.getNumRegions() == 3); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "dummy.1.wav"); REQUIRE(synth.getRegionView(2)->sampleId->filename() == "dummy.2.wav"); } TEST_CASE("[Files] Basic opcodes (regions_opcodes.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Regions/regions_opcodes.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->keyRange == Range(2, 14)); } TEST_CASE("[Files] Underscore opcodes (underscore_opcodes.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Regions/underscore_opcodes.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->loopMode == LoopMode::loop_sustain); } TEST_CASE("[Files] (regions_bad.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Regions/regions_bad.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "dummy.wav"); } TEST_CASE("[Files] Local include") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_local.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy.wav"); } TEST_CASE("[Files] Multiple includes") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/multiple_includes.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "dummy2.wav"); } TEST_CASE("[Files] Multiple includes with comments") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/multiple_includes_with_comments.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "dummy2.wav"); } TEST_CASE("[Files] Subdir include") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_subdir.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy_subdir.wav"); } TEST_CASE("[Files] Subdir include Win") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_subdir_win.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy_subdir.wav"); } TEST_CASE("[Files] Recursive include (with include guard)") { Synth synth; Parser& parser = synth.getParser(); parser.setRecursiveIncludeGuardEnabled(true); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_recursive.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy_recursive2.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "dummy_recursive1.wav"); } TEST_CASE("[Files] Include loops (with include guard)") { Synth synth; Parser& parser = synth.getParser(); parser.setRecursiveIncludeGuardEnabled(true); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_loop.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy_loop2.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "dummy_loop1.wav"); } TEST_CASE("[Files] Define test") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/defines.sfz"); REQUIRE(synth.getNumRegions() == 4); REQUIRE(synth.getRegionView(0)->keyRange == Range(36, 36)); REQUIRE(synth.getRegionView(1)->keyRange == Range(38, 38)); REQUIRE(synth.getRegionView(2)->keyRange == Range(42, 42)); REQUIRE(synth.getRegionView(3)->volume == -12.0f); } TEST_CASE("[Files] Group from AVL") { Synth synth; synth.loadSfzFile(fs::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)); } almostEqualRanges(synth.getRegionView(0)->velocityRange, { 1_norm, std::nextafter(27_norm, 0.0f) }); almostEqualRanges(synth.getRegionView(1)->velocityRange, { 27_norm, std::nextafter(53_norm, 0.0f) }); almostEqualRanges(synth.getRegionView(2)->velocityRange, { 53_norm, std::nextafter(78_norm, 0.0f) }); almostEqualRanges(synth.getRegionView(3)->velocityRange, { 78_norm, std::nextafter(103_norm, 0.0f) }); almostEqualRanges(synth.getRegionView(4)->velocityRange, { 103_norm, 127_norm }); } TEST_CASE("[Files] Full hierarchy") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); REQUIRE(synth.getNumRegions() == 8); for (int i = 0; i < synth.getNumRegions(); ++i) { REQUIRE(synth.getRegionView(i)->width == 0.4_a); } REQUIRE(synth.getRegionView(0)->pan == 0.3_a); REQUIRE(synth.getRegionView(0)->delay == 67); REQUIRE(synth.getRegionView(0)->keyRange == Range(60, 60)); REQUIRE(synth.getRegionView(1)->pan == 0.3_a); REQUIRE(synth.getRegionView(1)->delay == 67); REQUIRE(synth.getRegionView(1)->keyRange == Range(61, 61)); REQUIRE(synth.getRegionView(2)->pan == 0.3_a); REQUIRE(synth.getRegionView(2)->delay == 56); REQUIRE(synth.getRegionView(2)->keyRange == Range(50, 50)); REQUIRE(synth.getRegionView(3)->pan == 0.3_a); REQUIRE(synth.getRegionView(3)->delay == 56); REQUIRE(synth.getRegionView(3)->keyRange == Range(51, 51)); REQUIRE(synth.getRegionView(4)->pan == -0.1_a); REQUIRE(synth.getRegionView(4)->delay == 47); REQUIRE(synth.getRegionView(4)->keyRange == Range(40, 40)); REQUIRE(synth.getRegionView(5)->pan == -0.1_a); REQUIRE(synth.getRegionView(5)->delay == 47); REQUIRE(synth.getRegionView(5)->keyRange == Range(41, 41)); REQUIRE(synth.getRegionView(6)->pan == -0.1_a); REQUIRE(synth.getRegionView(6)->delay == 36); REQUIRE(synth.getRegionView(6)->keyRange == Range(30, 30)); REQUIRE(synth.getRegionView(7)->pan == -0.1_a); REQUIRE(synth.getRegionView(7)->delay == 36); REQUIRE(synth.getRegionView(7)->keyRange == Range(31, 31)); } TEST_CASE("[Files] Reloading files") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); REQUIRE(synth.getNumRegions() == 8); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); REQUIRE(synth.getNumRegions() == 8); } TEST_CASE("[Files] Full hierarchy with antislashes") { { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); REQUIRE(synth.getNumRegions() == 8); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "Regions/dummy.1.wav"); REQUIRE(synth.getRegionView(2)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(3)->sampleId->filename() == "Regions/dummy.1.wav"); REQUIRE(synth.getRegionView(4)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(5)->sampleId->filename() == "Regions/dummy.1.wav"); REQUIRE(synth.getRegionView(6)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(7)->sampleId->filename() == "Regions/dummy.1.wav"); } { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy_antislash.sfz"); REQUIRE(synth.getNumRegions() == 8); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "Regions/dummy.1.wav"); REQUIRE(synth.getRegionView(2)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(3)->sampleId->filename() == "Regions/dummy.1.wav"); REQUIRE(synth.getRegionView(4)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(5)->sampleId->filename() == "Regions/dummy.1.wav"); REQUIRE(synth.getRegionView(6)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(7)->sampleId->filename() == "Regions/dummy.1.wav"); } } TEST_CASE("[Files] Pizz basic") { Synth synth; synth.loadSfzFile(fs::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)); almostEqualRanges(synth.getRegionView(i)->velocityRange, { 97_norm, 127_norm }); REQUIRE(synth.getRegionView(i)->pitchKeycenter == 21); almostEqualRanges(synth.getRegionView(i)->ccConditions.getWithDefault(107), { 0_norm, std::nextafter(14_norm, 0.0f) }); // Fill in the gap from 13_norm to "almost 14_norm" } almostEqualRanges(synth.getRegionView(0)->randRange, { 0, 0.25 }); almostEqualRanges(synth.getRegionView(1)->randRange, { 0.25, 0.5 }); almostEqualRanges(synth.getRegionView(2)->randRange, { 0.5, 0.75 }); almostEqualRanges(synth.getRegionView(3)->randRange, { 0.75, 1.0 }); REQUIRE(synth.getRegionView(0)->sampleId->filename() == R"(../Samples/pizz/a0_vl4_rr1.wav)"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == R"(../Samples/pizz/a0_vl4_rr2.wav)"); REQUIRE(synth.getRegionView(2)->sampleId->filename() == R"(../Samples/pizz/a0_vl4_rr3.wav)"); REQUIRE(synth.getRegionView(3)->sampleId->filename() == R"(../Samples/pizz/a0_vl4_rr4.wav)"); } TEST_CASE("[Files] Channels (channels.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "mono_sample.wav"); REQUIRE(!synth.getRegionView(0)->isStereo()); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "stereo_sample.wav"); REQUIRE(synth.getRegionView(1)->isStereo()); } TEST_CASE("[Files] Generators and wavetables") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/wavetables.sfz"); REQUIRE(synth.getNumRegions() == 12); int regionNumber = 0; const Region* region = nullptr; // generator only region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "*sine"); REQUIRE(!region->isStereo()); REQUIRE(region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // generator with multi region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "*sine"); REQUIRE(region->isStereo()); REQUIRE(region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // explicit wavetable region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(!region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::On); // explicit wavetable with multi region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::On); // explicit disabled wavetable region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(!region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(!region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Off); // explicit disabled wavetable with multi region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(!region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(!region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Off); // implicit wavetable (sound file < 3000 frames and wavetable tags) region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "wavetables/surge.wav"); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // Parse oscillator=auto and same as above region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "wavetables/surge.wav"); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // implicit non wavetable (sound file < 3000 frames but no wavetable tags) region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "short_non_wavetable.wav"); REQUIRE(!region->isGenerator()); REQUIRE(!region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // implicit non-wavetable (sound file >= 3000 frames) region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "snare.wav"); REQUIRE(!region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(!region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // generator with multi=1 (single) region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "*sine"); REQUIRE(!region->isStereo()); REQUIRE(region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // generator with multi=2 (ring modulation) region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "*sine"); REQUIRE(!region->isStereo()); REQUIRE(region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); } TEST_CASE("[Files] Embedded wavetables") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/wavetables_embedded.sfz"); REQUIRE(synth.getNumRegions() == 7); int regionNumber = 0; const Region* region = nullptr; // explicit wavetable region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(!region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::On); // explicit wavetable with multi region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::On); // explicit disabled wavetable region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(!region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(!region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Off); // explicit disabled wavetable with multi region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "ramp_wave.wav"); REQUIRE(!region->isStereo()); REQUIRE(!region->isGenerator()); REQUIRE(!region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Off); // implicit wavetable (sound file < 3000 frames and wavetable tags) region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "wavetables/surge.wav"); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // Parse oscillator=auto and same as above region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "wavetables/surge.wav"); REQUIRE(!region->isGenerator()); REQUIRE(region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); // implicit non wavetable (sound file < 3000 frames but no wavetable tags) region = synth.getRegionView(regionNumber++); REQUIRE(region->sampleId->filename() == "short_non_wavetable.wav"); REQUIRE(!region->isGenerator()); REQUIRE(!region->isOscillator()); REQUIRE(region->oscillatorEnabled == OscillatorEnabled::Auto); } TEST_CASE("[Files] wrong (overlapping) replacement for defines") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/SpecificBugs/wrong-replacements.sfz"); REQUIRE( synth.getNumRegions() == 3 ); // Note: test checked to be wrong under Sforzando 1.961 // It is the shorter matching $-variable which matches among both. // The rest of the variable name creates some trailing junk text // which Sforzando accepts without warning. (eg. `key=57Edge`) REQUIRE( synth.getRegionView(0)->keyRange.getStart() == 57 ); REQUIRE( synth.getRegionView(0)->keyRange.getEnd() == 57 ); REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 ); REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 ); const ModKey target = ModKey::createNXYZ(ModId::Amplitude, synth.getRegionView(2)->getId()); const RegionCCView view(*synth.getRegionView(2), target); REQUIRE(!view.empty()); REQUIRE(view.valueAt(10) == Approx(0.34f).margin(1e-3)); } TEST_CASE("[Files] Specific bug: relative path with backslashes") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/SpecificBugs/win_backslashes.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->sampleId->filename() == R"(Xylo/Subfolder/closedhat.wav)"); } TEST_CASE("[Files] Default path") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/default_path.sfz"); REQUIRE(synth.getNumRegions() == 4); REQUIRE(synth.getRegionView(0)->sampleId->filename() == R"(DefaultPath/SubPath1/sample1.wav)"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == R"(DefaultPath/SubPath2/sample2.wav)"); REQUIRE(synth.getRegionView(2)->sampleId->filename() == R"(DefaultPath/SubPath1/sample1.wav)"); REQUIRE(synth.getRegionView(3)->sampleId->filename() == R"(DefaultPath/SubPath2/sample2.wav)"); } TEST_CASE("[Files] Default path reset when calling loadSfzFile again") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/default_path.sfz"); REQUIRE(synth.getNumRegions() == 4); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/default_path_reset.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->sampleId->filename() == R"(DefaultPath/SubPath2/sample2.wav)"); } TEST_CASE("[Files] Default path is ignored for generators") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/default_path_generator.sfz"); REQUIRE(synth.getNumRegions() == 1); REQUIRE(synth.getRegionView(0)->sampleId->filename() == R"(*sine)"); } TEST_CASE("[Files] Set CC applies properly") { Synth synth; const MidiState& midiState = synth.getResources().getMidiState(); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/set_cc.sfz"); REQUIRE(midiState.getCCValue(142) == 63_norm); REQUIRE(midiState.getCCValue(61) == 122_norm); } TEST_CASE("[Files] Set HDCC applies properly") { sfz::Synth synth; const MidiState& midiState = synth.getResources().getMidiState(); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/set_hdcc.sfz"); REQUIRE(midiState.getCCValue(142) == Approx(0.5678)); REQUIRE(midiState.getCCValue(61) == Approx(0.1234)); } TEST_CASE("[Files] Set RealCC applies properly") { sfz::Synth synth; const MidiState& midiState = synth.getResources().getMidiState(); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/set_realcc.sfz"); REQUIRE(midiState.getCCValue(142) == Approx(0.5678)); REQUIRE(midiState.getCCValue(61) == Approx(0.1234)); } TEST_CASE("[Files] Note and octave offsets") { Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/note_offset.sfz", R"( note_offset=1 key=63 sample=*sine lokey=50 hikey=55 pitch_keycenter=50 sample=*sine lokey=40 hikey=44 pitch_keycenter=40 xfin_lokey=36 xfin_hikey=40 xfout_lokey=44 xfout_hikey=48 sample=*sine note_offset=-1 key=63 sw_lokey=24 sw_hikey=28 sw_last=25 sw_up=25 sw_down=25 sw_previous=62 sample=*sine note_offset=1 octave_offset=1 key=63 sample=*sine note_offset=-1 octave_offset=-1 key=63 sample=*sine // Check that this does not reset either note or octave offset key=63 sample=*sine )"); REQUIRE( synth.getNumRegions() == 7 ); REQUIRE(synth.getRegionView(0)->keyRange == Range(64, 64)); REQUIRE( synth.getRegionView(0)->pitchKeycenter == 64 ); REQUIRE(synth.getRegionView(0)->crossfadeKeyInRange == Default::crossfadeKeyInRange); REQUIRE(synth.getRegionView(0)->crossfadeKeyOutRange == Default::crossfadeKeyOutRange); REQUIRE(synth.getRegionView(1)->keyRange == Range(51, 56)); REQUIRE( synth.getRegionView(1)->pitchKeycenter == 51 ); REQUIRE(synth.getRegionView(2)->keyRange == Range(41, 45)); REQUIRE( synth.getRegionView(2)->pitchKeycenter == 41 ); REQUIRE(synth.getRegionView(2)->crossfadeKeyInRange == Range(37, 41)); REQUIRE(synth.getRegionView(2)->crossfadeKeyOutRange == Range(45, 49)); REQUIRE(synth.getRegionView(3)->keyRange == Range(62, 62)); REQUIRE( synth.getRegionView(3)->lastKeyswitch ); REQUIRE( *synth.getRegionView(3)->lastKeyswitch == 24 ); REQUIRE( synth.getRegionView(3)->upKeyswitch ); REQUIRE( *synth.getRegionView(3)->upKeyswitch == 24 ); REQUIRE( synth.getRegionView(3)->downKeyswitch ); REQUIRE( *synth.getRegionView(3)->downKeyswitch == 24 ); REQUIRE( synth.getRegionView(3)->previousKeyswitch ); REQUIRE( *synth.getRegionView(3)->previousKeyswitch == 61 ); REQUIRE(synth.getRegionView(4)->keyRange == Range(76, 76)); REQUIRE( synth.getRegionView(4)->pitchKeycenter == 76 ); REQUIRE(synth.getRegionView(5)->keyRange == Range(50, 50)); REQUIRE( synth.getRegionView(5)->pitchKeycenter == 50 ); REQUIRE(synth.getRegionView(6)->keyRange == Range(50, 50)); REQUIRE( synth.getRegionView(6)->pitchKeycenter == 50 ); } TEST_CASE("[Files] Off modes") { Synth synth; AudioBuffer buffer { 2, 256 }; synth.setSamplesPerBlock(256); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_mode.sfz"); REQUIRE( synth.getNumRegions() == 3 ); synth.noteOn(0, 64, 63); synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices() == 2 ); const auto* fastVoice = synth.getVoiceView(0)->getRegion()->offMode == OffMode::fast ? synth.getVoiceView(0) : synth.getVoiceView(1) ; const auto* normalVoice = synth.getVoiceView(0)->getRegion()->offMode == OffMode::fast ? synth.getVoiceView(1) : synth.getVoiceView(0) ; synth.noteOn(100, 63, 63); synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices() == 3 ); REQUIRE( numPlayingVoices(synth) == 1 ); for (unsigned i = 0; i < 20; ++i) // Not enough for the "normal" voice to die synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices() == 2 ); REQUIRE( fastVoice->isFree() ); REQUIRE( !normalVoice->isFree() ); } TEST_CASE("[Files] Looped regions taken from files and possibly overriden") { Synth synth; synth.setSamplesPerBlock(256); synth.setSampleRate(44100); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/looped_regions.sfz"); REQUIRE( synth.getNumRegions() == 3 ); REQUIRE( synth.getRegionView(0)->loopMode == LoopMode::loop_continuous ); REQUIRE( synth.getRegionView(1)->loopMode == LoopMode::no_loop ); REQUIRE( synth.getRegionView(2)->loopMode == LoopMode::loop_continuous ); REQUIRE(synth.getRegionView(0)->loopRange == Range { 77554, 186581 }); REQUIRE(synth.getRegionView(1)->loopRange == Range { 77554, 186581 }); REQUIRE(synth.getRegionView(2)->loopRange == Range { 4, 124 }); } TEST_CASE("[Files] Looped regions can start at 0") { Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/loop_can_start_at_0.sfz", R"( sample=wavetable_with_loop_at_endings.wav )"); REQUIRE( synth.getNumRegions() == 1 ); REQUIRE( synth.getRegionView(0)->loopMode == LoopMode::loop_continuous ); REQUIRE( synth.getRegionView(0)->loopRange == Range { 0, synth.getRegionView(0)->sampleEnd } ); } TEST_CASE("[Synth] Release triggers automatically sets the loop mode") { sfz::Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/triggers_setting_loops.sfz", R"( sample=kick.wav pitch_keycenter=69 loop_mode=loop_sustain trigger=release sample=kick.wav pitch_keycenter=69 loop_mode=loop_sustain trigger=release_key sample=kick.wav pitch_keycenter=69 trigger=release loop_mode=loop_sustain sample=kick.wav pitch_keycenter=69 trigger=release_key loop_mode=loop_sustain sample=looped_flute.wav pitch_keycenter=69 trigger=release_key sample=kick.wav pitch_keycenter=69 trigger=release_key // These are normal and set to one_shot sample=kick.wav pitch_keycenter=69 trigger=release )"); REQUIRE( synth.getNumRegions() == 7 ); REQUIRE( synth.getRegionView(0)->loopMode == LoopMode::loop_sustain ); REQUIRE( synth.getRegionView(1)->loopMode == LoopMode::loop_sustain ); REQUIRE( synth.getRegionView(2)->loopMode == LoopMode::loop_sustain ); REQUIRE( synth.getRegionView(3)->loopMode == LoopMode::loop_sustain ); REQUIRE( synth.getRegionView(4)->loopMode == LoopMode::loop_continuous ); REQUIRE( synth.getRegionView(5)->loopMode == LoopMode::one_shot ); REQUIRE( synth.getRegionView(6)->loopMode == LoopMode::one_shot ); } TEST_CASE("[Files] Case sentitiveness") { const fs::path sfzFilePath = fs::current_path() / "tests/TestFiles/case_insensitive.sfz"; #if defined(_WIN32) const bool caseSensitiveFs = false; #elif defined(__APPLE__) const bool caseSensitiveFs = pathconf(sfzFilePath.string().c_str(), _PC_CASE_SENSITIVE) != 0; #else const bool caseSensitiveFs = true; #endif if (caseSensitiveFs) { Synth synth; synth.loadSfzFile(sfzFilePath); REQUIRE(synth.getNumRegions() == 4); REQUIRE(synth.getRegionView(0)->sampleId->filename() == "dummy1.wav"); REQUIRE(synth.getRegionView(1)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(2)->sampleId->filename() == "Regions/dummy.wav"); REQUIRE(synth.getRegionView(3)->sampleId->filename() == "Regions/dummy.wav"); } } TEST_CASE("[Files] Empty file") { Synth synth; Parser& parser = synth.getParser(); REQUIRE(!synth.loadSfzFile("")); REQUIRE(parser.getIncludedFiles().empty()); REQUIRE(!synth.loadSfzFile({})); REQUIRE(parser.getIncludedFiles().empty()); } TEST_CASE("[Files] Labels") { sfz::Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/labels.sfz"); auto keyLabels = synth.getKeyLabels(); auto ccLabels = synth.getCCLabels(); REQUIRE( keyLabels.size() == 2); REQUIRE( keyLabels[0].first == 12 ); REQUIRE( keyLabels[0].second == "Cymbals" ); REQUIRE( keyLabels[1].first == 65 ); REQUIRE( keyLabels[1].second == "Crash" ); REQUIRE( ccLabels.size() >= 2); REQUIRE( absl::c_find(ccLabels, CCNamePair { 54, "Gain" }) != ccLabels.end() ); REQUIRE( absl::c_find(ccLabels, CCNamePair { 2, "Other" }) != ccLabels.end() ); const std::string xmlMidnam = synth.exportMidnam(); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); } TEST_CASE("[Files] Switch labels") { sfz::Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/labels_sw.sfz"); const std::string xmlMidnam = synth.exportMidnam(); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); } TEST_CASE("[Files] Duplicate labels") { sfz::Synth synth; synth.loadSfzString( fs::current_path() / "tests/TestFiles/labels.sfz", R"( label_key60=Baz label_key60=Quux label_cc20=Foo label_cc20=Bar sample=*sine)"); auto keyLabels = synth.getKeyLabels(); auto ccLabels = synth.getCCLabels(); REQUIRE( keyLabels.size() == 1); REQUIRE( keyLabels[0].first == 60 ); REQUIRE( keyLabels[0].second == "Quux" ); REQUIRE( ccLabels.size() >= 1); REQUIRE( absl::c_find(ccLabels, CCNamePair { 20, "Bar" }) != ccLabels.end() ); const std::string xmlMidnam = synth.exportMidnam(); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); } TEST_CASE("[Files] Key center from audio file") { sfz::Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/sample_keycenter.sfz", R"( pitch_keycenter=sample oscillator=off sample=root_key_38.wav sample=root_key_62.wav sample=root_key_38.flac sample=root_key_62.flac pitch_keycenter=10 sample=root_key_62.flac key=10 sample=root_key_62.flac )"); REQUIRE(synth.getNumRegions() == 6); REQUIRE(synth.getRegionView(0)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(1)->pitchKeycenter == 62); REQUIRE(synth.getRegionView(2)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(3)->pitchKeycenter == 62); REQUIRE(synth.getRegionView(4)->pitchKeycenter == 10); REQUIRE(synth.getRegionView(5)->pitchKeycenter == 62); } TEST_CASE("[Files] Unused samples are cleared on reloading") { sfz::Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/unused_samples.sfz", R"( sample=*sine sample=kick.wav )"); REQUIRE(synth.getNumPreloadedSamples() == 1); // Same file path to reload synth.loadSfzString(fs::current_path() / "tests/TestFiles/unused_samples.sfz", R"( sample=*sine )"); REQUIRE(synth.getNumPreloadedSamples() == 0); } // FIXME: // this breaks on Github win32/win64/linux CI "sometimes" // but I can't reproduce it reliably. #if !defined(_WIN32) && !defined(_WIN64) && !defined(__APPLE__) TEST_CASE("[Files] Embedded sample data") { sfz::Synth synth1; sfz::Synth synth2; synth1.loadSfzFile(fs::current_path() / "tests/TestFiles/kick.sfz"); synth2.loadSfzFile(fs::current_path() / "tests/TestFiles/kick_embedded.sfz"); REQUIRE(synth1.getNumPreloadedSamples() == 1); REQUIRE(synth2.getNumPreloadedSamples() == 1); AudioBuffer buffer1 { 2, 256 }; AudioBuffer buffer2 { 2, 256 }; std::vector tmp1 (buffer1.getNumFrames()); std::vector tmp2 (buffer2.getNumFrames()); synth1.noteOn(0, 60, 100); synth2.noteOn(0, 60, 100); for (unsigned i = 0; i < 100; ++i) { synth1.renderBlock(buffer1); synth2.renderBlock(buffer2); const auto left1 = buffer1.getConstSpan(0); const auto left2 = buffer2.getConstSpan(0); std::copy(left1.begin(), left1.end(), tmp1.begin()); std::copy(left2.begin(), left2.end(), tmp2.begin()); REQUIRE(tmp1 == tmp2); const auto right1 = buffer1.getConstSpan(0); const auto right2 = buffer2.getConstSpan(0); std::copy(right1.begin(), right1.end(), tmp1.begin()); std::copy(right2.begin(), right2.end(), tmp2.begin()); REQUIRE(tmp1 == tmp2); } } #endif TEST_CASE("[Files] Key center from audio file, with embedded sample data") { sfz::Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/sample_keycenter_embedded.sfz"); REQUIRE(synth.getNumRegions() == 6); REQUIRE(synth.getRegionView(0)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(1)->pitchKeycenter == 62); REQUIRE(synth.getRegionView(2)->pitchKeycenter == 38); REQUIRE(synth.getRegionView(3)->pitchKeycenter == 62); REQUIRE(synth.getRegionView(4)->pitchKeycenter == 10); REQUIRE(synth.getRegionView(5)->pitchKeycenter == 62); }