diff --git a/clients/jack_client.cpp b/clients/jack_client.cpp index 46fff6b2c..ca610027b 100644 --- a/clients/jack_client.cpp +++ b/clients/jack_client.cpp @@ -78,34 +78,30 @@ int process(jack_nframes_t numFrames, void* arg) switch (midi::status(event.buffer[0])) { case midi::noteOff: noteoff: - // DBG("[MIDI] Note " << +event.buffer[1] << " OFF at time " << event.time); synth->noteOff(event.time, event.buffer[1], event.buffer[2]); break; case midi::noteOn: if (event.buffer[2] == 0) goto noteoff; - // DBG("[MIDI] Note " << +event.buffer[1] << " ON at time " << event.time); synth->noteOn(event.time, event.buffer[1], event.buffer[2]); break; case midi::polyphonicPressure: - // DBG("[MIDI] Polyphonic pressure on at time " << event.time); + // Not implemented break; case midi::controlChange: - // DBG("[MIDI] CC " << +event.buffer[1] << " at time " << event.time); synth->cc(event.time, event.buffer[1], event.buffer[2]); break; case midi::programChange: - // DBG("[MIDI] Program change at time " << event.time); + // Not implemented break; case midi::channelPressure: - // DBG("[MIDI] Channel pressure at time " << event.time); + synth->aftertouch(event.time, event.buffer[1]); break; case midi::pitchBend: synth->pitchWheel(event.time, midi::buildAndCenterPitch(event.buffer[1], event.buffer[2])); - // DBG("[MIDI] Pitch bend at time " << event.time); break; case midi::systemMessage: - // DBG("[MIDI] System message at time " << event.time); + // Not implemented break; } } diff --git a/common.mk b/common.mk index 81d513df8..06d9fc0d2 100644 --- a/common.mk +++ b/common.mk @@ -57,6 +57,7 @@ SFIZZ_SOURCES = \ src/sfizz/modulations/ModKeyHash.cpp \ src/sfizz/modulations/ModMatrix.cpp \ src/sfizz/modulations/sources/ADSREnvelope.cpp \ + src/sfizz/modulations/sources/ChannelAftertouch.cpp \ src/sfizz/modulations/sources/Controller.cpp \ src/sfizz/modulations/sources/FlexEnvelope.cpp \ src/sfizz/modulations/sources/LFO.cpp \ diff --git a/external/atomic_queue/include/atomic_queue/atomic_queue.h b/external/atomic_queue/include/atomic_queue/atomic_queue.h index 1319f6f9b..21dc029bf 100644 --- a/external/atomic_queue/include/atomic_queue/atomic_queue.h +++ b/external/atomic_queue/include/atomic_queue/atomic_queue.h @@ -131,7 +131,7 @@ class AtomicQueueCommon { // The special member functions are not thread-safe. - AtomicQueueCommon() noexcept = default; + AtomicQueueCommon() = default; AtomicQueueCommon(AtomicQueueCommon const& b) noexcept : head_(b.head_.load(X)) @@ -403,7 +403,7 @@ class AtomicQueue2 : public AtomicQueueCommonsynth, + (int)ev->time.frames, + msg[1]); + break; case LV2_MIDI_MSG_BENDER: sfizz_send_pitch_wheel(self->synth, (int)ev->time.frames, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d548788c6..c82a21c87 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,6 +33,7 @@ set(SFIZZ_HEADERS sfizz/modulations/ModMatrix.h sfizz/modulations/ModGenerator.h sfizz/modulations/sources/ADSREnvelope.h + sfizz/modulations/sources/ChannelAftertouch.h sfizz/modulations/sources/Controller.h sfizz/modulations/sources/FlexEnvelope.h sfizz/modulations/sources/LFO.h @@ -157,9 +158,10 @@ set(SFIZZ_SOURCES sfizz/modulations/ModKey.cpp sfizz/modulations/ModKeyHash.cpp sfizz/modulations/ModMatrix.cpp + sfizz/modulations/sources/ADSREnvelope.cpp + sfizz/modulations/sources/ChannelAftertouch.cpp sfizz/modulations/sources/Controller.cpp sfizz/modulations/sources/FlexEnvelope.cpp - sfizz/modulations/sources/ADSREnvelope.cpp sfizz/modulations/sources/LFO.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 451a5bcc7..aa3442515 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -24,6 +24,19 @@ enum class Oversampling: int { x8 = 8 }; +enum ExtendedCCs { + pitchBend = 128, + channelAftertouch, + polyphonicAftertouch, + noteOnVelocity, + noteOffVelocity, + keyboardNoteNumber, + keyboardNoteGate, + unipolarRandom, + bipolarRandom, + alternate +}; + namespace config { constexpr float defaultSampleRate { 48000 }; constexpr float maxSampleRate { 192000 }; diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 03e65a9e1..1f6cc4b5d 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -56,28 +56,33 @@ void sfz::MidiState::setSampleRate(float sampleRate) noexcept void sfz::MidiState::advanceTime(int numSamples) noexcept { + auto clearEvents = [] (EventVector& events) { + ASSERT(!events.empty()); // CC event vectors should never be empty + events.front().value = events.back().value; + events.front().delay = 0; + events.resize(1); + }; + internalClock += numSamples; - for (auto& ccEvents : cc) { - ASSERT(!ccEvents.empty()); // CC event vectors should never be empty - ccEvents.front().value = ccEvents.back().value; - ccEvents.front().delay = 0; - ccEvents.resize(1); - } - ASSERT(!pitchEvents.empty()); - pitchEvents.front().value = pitchEvents.back().value; - pitchEvents.front().delay = 0; - pitchEvents.resize(1); + for (auto& ccEvents : cc) + clearEvents(ccEvents); + + clearEvents(pitchEvents); + clearEvents(channelAftertouchEvents); } void sfz::MidiState::setSamplesPerBlock(int samplesPerBlock) noexcept { + auto updateEventBufferSize = [=] (EventVector& events) { + events.shrink_to_fit(); + events.reserve(samplesPerBlock); + }; this->samplesPerBlock = samplesPerBlock; - for (auto& ccEvents : cc) { - ccEvents.shrink_to_fit(); - ccEvents.reserve(samplesPerBlock); - } - pitchEvents.shrink_to_fit(); - pitchEvents.reserve(samplesPerBlock); + for (auto& ccEvents : cc) + updateEventBufferSize(ccEvents); + + updateEventBufferSize(pitchEvents); + updateEventBufferSize(channelAftertouchEvents); } float sfz::MidiState::getNoteDuration(int noteNumber, int delay) const @@ -100,15 +105,19 @@ float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept return lastNoteVelocities[noteNumber]; } +void sfz::MidiState::insertEventInVector(EventVector& events, int delay, float value) +{ + const auto insertionPoint = absl::c_upper_bound(events, delay, MidiEventDelayComparator {}); + if (insertionPoint == events.end() || insertionPoint->delay != delay) + events.insert(insertionPoint, { delay, value }); + else + insertionPoint->value = value; +} + void sfz::MidiState::pitchBendEvent(int delay, float pitchBendValue) noexcept { ASSERT(pitchBendValue >= -1.0f && pitchBendValue <= 1.0f); - - const auto insertionPoint = absl::c_upper_bound(pitchEvents, delay, MidiEventDelayComparator {}); - if (insertionPoint == pitchEvents.end() || insertionPoint->delay != delay) - pitchEvents.insert(insertionPoint, { delay, pitchBendValue }); - else - insertionPoint->value = pitchBendValue; + insertEventInVector(pitchEvents, delay, pitchBendValue); } float sfz::MidiState::getPitchBend() const noexcept @@ -117,20 +126,27 @@ float sfz::MidiState::getPitchBend() const noexcept return pitchEvents.back().value; } +void sfz::MidiState::channelAftertouchEvent(int delay, float aftertouch) noexcept +{ + ASSERT(aftertouch >= -1.0f && aftertouch <= 1.0f); + insertEventInVector(channelAftertouchEvents, delay, aftertouch); +} + +float sfz::MidiState::getChannelAftertouch() const noexcept +{ + ASSERT(channelAftertouchEvents.size() > 0); + return channelAftertouchEvents.back().value; +} + void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept { ASSERT(ccValue >= 0.0 && ccValue <= 1.0); - const auto insertionPoint = absl::c_upper_bound(cc[ccNumber], delay, MidiEventDelayComparator {}); - if (insertionPoint == cc[ccNumber].end() || insertionPoint->delay != delay) - cc[ccNumber].insert(insertionPoint, { delay, ccValue }); - else - insertionPoint->value = ccValue; + insertEventInVector(cc[ccNumber], delay, ccValue); } float sfz::MidiState::getCCValue(int ccNumber) const noexcept { ASSERT(ccNumber >= 0 && ccNumber < config::numCCs); - return cc[ccNumber].back().value; } @@ -139,13 +155,16 @@ void sfz::MidiState::reset() noexcept for (auto& velocity: lastNoteVelocities) velocity = 0; - for (auto& ccEvents : cc) { - ccEvents.clear(); - ccEvents.push_back({ 0, 0.0f }); - } + auto clearEvents = [] (EventVector& events) { + events.clear(); + events.push_back({ 0, 0.0f }); + }; + + for (auto& ccEvents : cc) + clearEvents(ccEvents); - pitchEvents.clear(); - pitchEvents.push_back({ 0, 0.0f }); + clearEvents(pitchEvents); + clearEvents(channelAftertouchEvents); activeNotes = 0; internalClock = 0; @@ -173,3 +192,8 @@ const sfz::EventVector& sfz::MidiState::getPitchEvents() const noexcept { return pitchEvents; } + +const sfz::EventVector& sfz::MidiState::getChannelAftertouchEvents() const noexcept +{ + return channelAftertouchEvents; +} diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index f48755106..bbdf58e1b 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -98,6 +98,20 @@ class MidiState */ float getPitchBend() const noexcept; + /** + * @brief Register a channel aftertouch event + * + * @param aftertouch + */ + void channelAftertouchEvent(int delay, float aftertouch) noexcept; + + /** + * @brief Get the channel aftertouch status + + * @return int + */ + float getChannelAftertouch() const noexcept; + /** * @brief Register a CC event * @@ -135,8 +149,19 @@ class MidiState const EventVector& getCCEvents(int ccIdx) const noexcept; const EventVector& getPitchEvents() const noexcept; + const EventVector& getChannelAftertouchEvents() const noexcept; private: + + /** + * @brief Insert events in a sorted event vector. + * + * @param events + * @param delay + * @param value + */ + void insertEventInVector(EventVector& events, int delay, float value); + int activeNotes { 0 }; /** @@ -175,6 +200,12 @@ class MidiState * @brief Pitch bend status */ EventVector pitchEvents; + + /** + * @brief Aftertouch status + */ + EventVector channelAftertouchEvents; + float sampleRate { config::defaultSampleRate }; int samplesPerBlock { config::defaultSamplesPerBlock }; unsigned internalClock { 0 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index fbbee9396..a1defa97b 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -652,6 +652,19 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) processGenericCc(opcode, Default::filterResonanceModRange, ModKey::createNXYZ(ModId::FilResonance, id, filterIndex)); } break; + case hash("cutoff&_chanaft"): + { + const auto filterIndex = opcode.parameters.front() - 1; + if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters)) + return false; + + if (auto value = readOpcode(opcode.value, Default::filterCutoffModRange)) { + const ModKey source = ModKey::createNXYZ(ModId::ChannelAftertouch); + const ModKey target = ModKey::createNXYZ(ModId::FilCutoff, id, filterIndex); + getOrCreateConnection(source, target).sourceDepth = *value; + } + } + break; case hash("fil&_keytrack"): // also fil_keytrack { const auto filterIndex = opcode.parameters.front() - 1; diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index b26f65607..ace6733cb 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -54,6 +54,7 @@ struct MidiEvent { int delay; float value; }; + using EventVector = std::vector; struct MidiEventDelayComparator { diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b82cb372a..00ee4beeb 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -63,6 +63,7 @@ Synth::Impl::Impl() genLFO_.reset(new LFOSource(voiceManager_)); genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceManager_)); genADSREnvelope_.reset(new ADSREnvelopeSource(voiceManager_, resources_.midiState)); + genChannelAftertouch_.reset(new ChannelAftertouchSource(voiceManager_, resources_.midiState)); } Synth::Impl::~Impl() @@ -1214,11 +1215,26 @@ void Synth::pitchWheel(int delay, int pitch) noexcept voice.registerPitchWheel(delay, normalizedPitch); } } -void Synth::aftertouch(int /* delay */, uint8_t /* aftertouch */) noexcept + +void Synth::aftertouch(int delay, uint8_t aftertouch) noexcept { Impl& impl = *impl_; ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration }; + + const auto normalizedAftertouch = normalize7Bits(aftertouch); + impl.resources_.midiState.channelAftertouchEvent(delay, normalizedAftertouch); + + for (auto& region : impl.regions_) { + region->registerAftertouch(aftertouch); + } + + for (auto& voice : impl.voiceManager_) { + voice.registerAftertouch(delay, aftertouch); + } + + impl.performHdcc(delay, ExtendedCCs::channelAftertouch, normalizedAftertouch, false); } + void Synth::tempo(int delay, float secondsPerBeat) noexcept { Impl& impl = *impl_; @@ -1226,6 +1242,7 @@ void Synth::tempo(int delay, float secondsPerBeat) noexcept impl.resources_.beatClock.setTempo(delay, secondsPerBeat); } + void Synth::timeSignature(int delay, int beatsPerBar, int beatUnit) { Impl& impl = *impl_; @@ -1233,6 +1250,7 @@ void Synth::timeSignature(int delay, int beatsPerBar, int beatUnit) impl.resources_.beatClock.setTimeSignature(delay, TimeSignature(beatsPerBar, beatUnit)); } + void Synth::timePosition(int delay, int bar, double barBeat) { Impl& impl = *impl_; @@ -1240,6 +1258,7 @@ void Synth::timePosition(int delay, int bar, double barBeat) impl.resources_.beatClock.setTimePosition(delay, BBT(bar, barBeat)); } + void Synth::playbackState(int delay, int playbackState) { Impl& impl = *impl_; @@ -1253,16 +1272,19 @@ int Synth::getNumRegions() const noexcept Impl& impl = *impl_; return static_cast(impl.regions_.size()); } + int Synth::getNumGroups() const noexcept { Impl& impl = *impl_; return impl.numGroups_; } + int Synth::getNumMasters() const noexcept { Impl& impl = *impl_; return impl.numMasters_; } + int Synth::getNumCurves() const noexcept { Impl& impl = *impl_; @@ -1563,6 +1585,9 @@ void Synth::Impl::setupModMatrix() case ModId::FilEG: gen = genADSREnvelope_.get(); break; + case ModId::ChannelAftertouch: + gen = genChannelAftertouch_.get(); + break; default: DBG("[sfizz] Have unknown type of source generator"); break; diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 0e2510240..185d6b115 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -8,6 +8,7 @@ #include "modulations/sources/ADSREnvelope.h" #include "modulations/sources/Controller.h" #include "modulations/sources/FlexEnvelope.h" +#include "modulations/sources/ChannelAftertouch.h" #include "modulations/sources/LFO.h" #include "utility/BitArray.h" @@ -265,6 +266,7 @@ struct Synth::Impl final: public Parser::Listener { std::unique_ptr genLFO_; std::unique_ptr genFlexEnvelope_; std::unique_ptr genADSREnvelope_; + std::unique_ptr genChannelAftertouch_; // Settings per voice struct { diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index 388da1897..374b08879 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -36,6 +36,8 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice; case ModId::FilEG: return kModIsPerVoice; + case ModId::ChannelAftertouch: + return kModIsPerCycle; // targets case ModId::MasterAmplitude: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index f4c45965e..055dacdc9 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -26,6 +26,7 @@ enum class ModId : int { AmpEG, PitchEG, FilEG, + ChannelAftertouch, _SourcesEnd, diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 474ca6228..1cb95991a 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -105,6 +105,8 @@ std::string ModKey::toString() const return absl::StrCat("PitchEG {", region_.number(), "}"); case ModId::FilEG: return absl::StrCat("FilterEG {", region_.number(), "}"); + case ModId::ChannelAftertouch: + return absl::StrCat("ChannelAftertouch"); case ModId::MasterAmplitude: return absl::StrCat("MasterAmplitude {", region_.number(), "}"); diff --git a/src/sfizz/modulations/ModKey.h b/src/sfizz/modulations/ModKey.h index 5653386ff..9498ee39d 100644 --- a/src/sfizz/modulations/ModKey.h +++ b/src/sfizz/modulations/ModKey.h @@ -29,7 +29,7 @@ class ModKey { : id_(id), region_(region), params_(params), flags_(ModIds::flags(id_)) {} static ModKey createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float step); - static ModKey createNXYZ(ModId id, NumericId region, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0); + static ModKey createNXYZ(ModId id, NumericId region = {}, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0); explicit operator bool() const noexcept { return id_ != ModId(); } diff --git a/src/sfizz/modulations/sources/ChannelAftertouch.cpp b/src/sfizz/modulations/sources/ChannelAftertouch.cpp new file mode 100644 index 000000000..3bee93dfa --- /dev/null +++ b/src/sfizz/modulations/sources/ChannelAftertouch.cpp @@ -0,0 +1,42 @@ +// 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 "ChannelAftertouch.h" +#include "../../ModifierHelpers.h" +#include "../../ADSREnvelope.h" + +// TODO(jpc): also matrix the ampeg + +namespace sfz { + +ChannelAftertouchSource::ChannelAftertouchSource(VoiceManager& manager, MidiState& state) + : voiceManager_(manager), midiState_(state) +{ +} + +void ChannelAftertouchSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + UNUSED(sourceKey); + UNUSED(voiceId); + UNUSED(delay); +} + +void ChannelAftertouchSource::release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + UNUSED(sourceKey); + UNUSED(voiceId); + UNUSED(delay); +} + +void ChannelAftertouchSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + UNUSED(sourceKey); + UNUSED(voiceId); + const EventVector& events = midiState_.getChannelAftertouchEvents(); + linearEnvelope(events, buffer, [](float x) { return x; }); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/ChannelAftertouch.h b/src/sfizz/modulations/sources/ChannelAftertouch.h new file mode 100644 index 000000000..6ed9ac2c2 --- /dev/null +++ b/src/sfizz/modulations/sources/ChannelAftertouch.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../ModGenerator.h" +#include "../../VoiceManager.h" +#include "../../MidiState.h" + +namespace sfz { +class Synth; + +class ChannelAftertouchSource : public ModGenerator { +public: + explicit ChannelAftertouchSource(VoiceManager &manager, MidiState& state); + void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; + void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; + +private: + VoiceManager& voiceManager_; + MidiState& midiState_; +}; + +} // namespace sfz diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 0e1a863b2..72c38cb6b 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -339,3 +339,18 @@ TEST_CASE("[Modulations] Override the default pan controller") R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")", })); } + +TEST_CASE("[Modulations] Aftertouch connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine cutoff_chanaft=1000 + sample=*sine cutoff2_chanaft=1000 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createDefaultGraph({ + R"("ChannelAftertouch" -> "FilterCutoff {0, N=1}")", + R"("ChannelAftertouch" -> "FilterCutoff {1, N=2}")", + }, 2)); +} diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index b9bf7ab82..46a42651f 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1424,3 +1424,18 @@ TEST_CASE("[Synth] Send CC vs. Automate CC") REQUIRE(synth.getNumActiveVoices() == 1); } } + +TEST_CASE("[Keyswitches] Trigger from aftertouch extended CC") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/aftertouch_trigger.sfz", R"( + start_locc129=100 start_hicc129=127 sample=*saw + )"); + REQUIRE(synth.getNumActiveVoices() == 0); + synth.aftertouch(0, 90); + REQUIRE(synth.getNumActiveVoices() == 0); + synth.aftertouch(0, 110); + REQUIRE(synth.getNumActiveVoices() == 1); + synth.aftertouch(0, 120); + REQUIRE(synth.getNumActiveVoices() == 2); +}