diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 289dc707..d4ff78ec 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -156,5 +156,7 @@ namespace Default // ***** SFZ v2 ******** constexpr bool checkSustain { true }; // sustain_sw constexpr bool checkSostenuto { true }; // sostenuto_sw + constexpr Range octaveOffsetRange { -10, 10 }; // octave_offset + constexpr Range noteOffsetRange { -127, 127 }; // note_offset } } diff --git a/src/sfizz/Range.h b/src/sfizz/Range.h index 2af64b4f..b52224a7 100644 --- a/src/sfizz/Range.h +++ b/src/sfizz/Range.h @@ -90,7 +90,7 @@ public: * @param value * @return Type */ - Type clamp(Type value) const noexcept { return ::clamp(value, _start, _end); } + constexpr Type clamp(Type value) const noexcept { return ::clamp(value, _start, _end); } /** * @brief Checks if a value is in the range, including the endpoints * @@ -137,6 +137,13 @@ bool operator==(const sfz::Range& lhs, const sfz::Range& rhs) return (lhs.getStart() == rhs.getStart()) && (lhs.getEnd() == rhs.getEnd()); } +template +bool operator!=(const sfz::Range& lhs, const sfz::Range& rhs) +{ + return (lhs.getStart() != rhs.getStart()) || (lhs.getEnd() != rhs.getEnd()); +} + + template bool operator==(const sfz::Range& lhs, const std::pair& rhs) { diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index db18e5c9..e7bc0663 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -807,3 +807,57 @@ float sfz::Region::velocityCurve(uint8_t velocity) const noexcept return gain; } + +constexpr uint8_t offsetAndClamp(uint8_t key, int offset, sfz::Range range) +{ + const int offsetKey { key + offset }; + if (offsetKey > std::numeric_limits::max()) + return range.getEnd(); + if (offsetKey < std::numeric_limits::min()) + return range.getStart(); + + return range.clamp(static_cast(offsetKey)); +} + +void sfz::Region::offsetAllKeys(int offset) noexcept +{ + // Offset key range + if (keyRange != Default::keyRange) { + const auto start = keyRange.getStart(); + const auto end = keyRange.getEnd(); + keyRange.setStart(offsetAndClamp(start, offset, Default::keyRange)); + keyRange.setEnd(offsetAndClamp(end, offset, Default::keyRange)); + } + pitchKeycenter = offsetAndClamp(pitchKeycenter, offset, Default::keyRange); + + // Offset key switches + if (keyswitchRange != Default::keyRange) { + const auto start = keyswitchRange.getStart(); + const auto end = keyswitchRange.getEnd(); + keyswitchRange.setStart(offsetAndClamp(start, offset, Default::keyRange)); + keyswitchRange.setEnd(offsetAndClamp(end, offset, Default::keyRange)); + } + if (keyswitchUp) + keyswitchUp = offsetAndClamp(*keyswitchUp, offset, Default::keyRange); + if (keyswitch) + keyswitch = offsetAndClamp(*keyswitch, offset, Default::keyRange); + if (keyswitchDown) + keyswitchDown = offsetAndClamp(*keyswitchDown, offset, Default::keyRange); + if (previousNote) + previousNote = offsetAndClamp(*previousNote, offset, Default::keyRange); + + // Offset crossfade ranges + if (crossfadeKeyInRange != Default::crossfadeKeyInRange) { + const auto start = crossfadeKeyInRange.getStart(); + const auto end = crossfadeKeyInRange.getEnd(); + crossfadeKeyInRange.setStart(offsetAndClamp(start, offset, Default::keyRange)); + crossfadeKeyInRange.setEnd(offsetAndClamp(end, offset, Default::keyRange)); + } + + if (crossfadeKeyOutRange != Default::crossfadeKeyOutRange) { + const auto start = crossfadeKeyOutRange.getStart(); + const auto end = crossfadeKeyOutRange.getEnd(); + crossfadeKeyOutRange.setStart(offsetAndClamp(start, offset, Default::keyRange)); + crossfadeKeyOutRange.setEnd(offsetAndClamp(end, offset, Default::keyRange)); + } +} diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 6c7a3653..6621b3af 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -220,6 +220,8 @@ struct Region { */ bool parseOpcode(const Opcode& opcode); + void offsetAllKeys(int offset) noexcept; + uint32_t loopStart(Oversampling factor = x1) const noexcept; uint32_t loopEnd(Oversampling factor = x1) const noexcept; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b0e145b7..a0a0e3a3 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -114,6 +114,9 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) parseOpcodes(groupOpcodes); parseOpcodes(regionOpcodes); + if (octaveOffset != 0 || noteOffset != 0) + lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset); + regions.push_back(std::move(lastRegion)); } @@ -186,6 +189,12 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) defaultPath = absl::StrReplaceAll(trim(member.value), { { "\\", "/" } }); DBG("Changing default sample path to " << defaultPath); break; + case hash("note_offset"): + setValueFromOpcode(member, noteOffset, Default::noteOffsetRange); + break; + case hash("octave_offset"): + setValueFromOpcode(member, octaveOffset, Default::octaveOffsetRange); + break; default: // Unsupported control opcode DBG("Unsupported control opcode: " << member.opcode); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3fc5a3ed..5dca692f 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -455,8 +455,10 @@ private: Resources resources; MidiState midiState; - // default_path + // Control opcodes std::string defaultPath { "" }; + int noteOffset { 0 }; + int octaveOffset { 0 }; LEAK_DETECTOR(Synth); }; diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index bd709e92..60285e5f 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -365,3 +365,46 @@ TEST_CASE("[Files] Set CC applies properly to all channels") REQUIRE(synth.getMidiState().getCCValue(channel, 61) == 122); } } + + +TEST_CASE("[Files] Note and octave offsets") +{ + sfz::Synth synth; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/note_offset.sfz"); + REQUIRE( synth.getNumRegions() == 7 ); + + REQUIRE( synth.getRegionView(0)->keyRange == sfz::Range(64, 64) ); + REQUIRE( synth.getRegionView(0)->pitchKeycenter == 64 ); + REQUIRE( synth.getRegionView(0)->keyswitchRange == sfz::Default::keyRange ); + REQUIRE( synth.getRegionView(0)->crossfadeKeyInRange == sfz::Default::crossfadeKeyInRange ); + REQUIRE( synth.getRegionView(0)->crossfadeKeyOutRange == sfz::Default::crossfadeKeyOutRange ); + + REQUIRE( synth.getRegionView(1)->keyRange == sfz::Range(51, 56) ); + REQUIRE( synth.getRegionView(1)->pitchKeycenter == 51 ); + + REQUIRE( synth.getRegionView(2)->keyRange == sfz::Range(41, 45) ); + REQUIRE( synth.getRegionView(2)->pitchKeycenter == 41 ); + REQUIRE( synth.getRegionView(2)->crossfadeKeyInRange == sfz::Range(37, 41) ); + REQUIRE( synth.getRegionView(2)->crossfadeKeyOutRange == sfz::Range(45, 49) ); + + REQUIRE( synth.getRegionView(3)->keyRange == sfz::Range(62, 62) ); + REQUIRE( synth.getRegionView(3)->keyswitchRange == sfz::Range(23, 27) ); + REQUIRE( synth.getRegionView(3)->keyswitch ); + REQUIRE( *synth.getRegionView(3)->keyswitch == 24 ); + REQUIRE( synth.getRegionView(3)->keyswitchUp ); + REQUIRE( *synth.getRegionView(3)->keyswitchUp == 24 ); + REQUIRE( synth.getRegionView(3)->keyswitchDown ); + REQUIRE( *synth.getRegionView(3)->keyswitchDown == 24 ); + REQUIRE( synth.getRegionView(3)->previousNote ); + REQUIRE( *synth.getRegionView(3)->previousNote == 61 ); + + REQUIRE( synth.getRegionView(4)->keyRange == sfz::Range(76, 76) ); + REQUIRE( synth.getRegionView(4)->pitchKeycenter == 76 ); + + REQUIRE( synth.getRegionView(5)->keyRange == sfz::Range(50, 50) ); + REQUIRE( synth.getRegionView(5)->pitchKeycenter == 50 ); + + REQUIRE( synth.getRegionView(6)->keyRange == sfz::Range(50, 50) ); + REQUIRE( synth.getRegionView(6)->pitchKeycenter == 50 ); +} + diff --git a/tests/TestFiles/note_offset.sfz b/tests/TestFiles/note_offset.sfz index d372cc30..40760e49 100644 --- a/tests/TestFiles/note_offset.sfz +++ b/tests/TestFiles/note_offset.sfz @@ -1,16 +1,12 @@ 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=36 xfout_hikey=40 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 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=36 xfout_hikey=40 sample=*sine + 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 - lokey=50 hikey=55 pitch_keycenter=50 sample=*sine - lokey=40 hikey=44 pitch_keycenter=40 xfin_lokey=36 xfin_hikey=40 xfout_lokey=36 xfout_hikey=40 sample=*sine note_offset=-1 octave_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=36 xfout_hikey=40 sample=*sine + // Check that this does not reset either note or octave offset + key=63 sample=*sine