diff --git a/clients/jack_client.cpp b/clients/jack_client.cpp index ca610027b..23975aa33 100644 --- a/clients/jack_client.cpp +++ b/clients/jack_client.cpp @@ -86,7 +86,7 @@ int process(jack_nframes_t numFrames, void* arg) synth->noteOn(event.time, event.buffer[1], event.buffer[2]); break; case midi::polyphonicPressure: - // Not implemented + synth->polyAftertouch(event.time, event.buffer[1], event.buffer[2]); break; case midi::controlChange: synth->cc(event.time, event.buffer[1], event.buffer[2]); diff --git a/common.mk b/common.mk index 21ca24b3a..d8ee53938 100644 --- a/common.mk +++ b/common.mk @@ -59,6 +59,7 @@ SFIZZ_SOURCES = \ src/sfizz/modulations/ModMatrix.cpp \ src/sfizz/modulations/sources/ADSREnvelope.cpp \ src/sfizz/modulations/sources/ChannelAftertouch.cpp \ + src/sfizz/modulations/sources/PolyAftertouch.cpp \ src/sfizz/modulations/sources/Controller.cpp \ src/sfizz/modulations/sources/FlexEnvelope.cpp \ src/sfizz/modulations/sources/LFO.cpp \ diff --git a/plugins/lv2/sfizz.cpp b/plugins/lv2/sfizz.cpp index f597d2e9d..a0c94a0bc 100644 --- a/plugins/lv2/sfizz.cpp +++ b/plugins/lv2/sfizz.cpp @@ -640,6 +640,12 @@ sfizz_lv2_process_midi_event(sfizz_plugin_t *self, const LV2_Atom_Event *ev) (int)ev->time.frames, msg[1]); break; + case LV2_MIDI_MSG_NOTE_PRESSURE: + sfizz_send_poly_aftertouch(self->synth, + (int)ev->time.frames, + (int)msg[1], + msg[2]); + break; case LV2_MIDI_MSG_BENDER: sfizz_send_pitch_wheel(self->synth, (int)ev->time.frames, diff --git a/plugins/vst/SfizzVstProcessor.cpp b/plugins/vst/SfizzVstProcessor.cpp index 6dd841a77..1ba119236 100644 --- a/plugins/vst/SfizzVstProcessor.cpp +++ b/plugins/vst/SfizzVstProcessor.cpp @@ -465,9 +465,13 @@ void SfizzVstProcessor::processEvents(Vst::IEventList& events) _noteEventsCurrentCycle[pitch] = 0.0f; break; } - // case Vst::Event::kPolyPressureEvent: - // synth.aftertouch(e.sampleOffset, convertVelocityFromFloat(e.polyPressure.pressure)); - // break; + case Vst::Event::kPolyPressureEvent: { + int pitch = e.polyPressure.pitch; + if (pitch < 0 || pitch >= 128) + break; + synth.polyAftertouch(e.sampleOffset, pitch, convertVelocityFromFloat(e.polyPressure.pressure)); + break; + } } } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bc1ad2ccd..bd21c4ccb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,7 @@ set(SFIZZ_HEADERS sfizz/modulations/ModGenerator.h sfizz/modulations/sources/ADSREnvelope.h sfizz/modulations/sources/ChannelAftertouch.h + sfizz/modulations/sources/PolyAftertouch.h sfizz/modulations/sources/Controller.h sfizz/modulations/sources/FlexEnvelope.h sfizz/modulations/sources/LFO.h @@ -166,6 +167,7 @@ set(SFIZZ_SOURCES sfizz/modulations/ModMatrix.cpp sfizz/modulations/sources/ADSREnvelope.cpp sfizz/modulations/sources/ChannelAftertouch.cpp + sfizz/modulations/sources/PolyAftertouch.cpp sfizz/modulations/sources/Controller.cpp sfizz/modulations/sources/FlexEnvelope.cpp sfizz/modulations/sources/LFO.cpp diff --git a/src/sfizz.h b/src/sfizz.h index fa60ea99a..056a2793e 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -430,7 +430,7 @@ SFIZZ_EXPORTED_API void sfizz_automate_hdcc(sfizz_synth_t* synth, int delay, int SFIZZ_EXPORTED_API void sfizz_send_pitch_wheel(sfizz_synth_t* synth, int delay, int pitch); /** - * @brief Send an aftertouch event. (CURRENTLY UNIMPLEMENTED) + * @brief Send an aftertouch event. * @since 0.2.0 * * @param synth The synth. @@ -443,6 +443,22 @@ SFIZZ_EXPORTED_API void sfizz_send_pitch_wheel(sfizz_synth_t* synth, int delay, */ SFIZZ_EXPORTED_API void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, char aftertouch); +/** + * @brief Send a polyphonic aftertouch event. This feature is experimental and needs more testing + * in the internal engine. + * @since 0.6.0 + * + * @param synth The synth. + * @param delay The delay at which the event occurs; this should be lower + * than the size of the block in the next call to renderBlock(). + * @param note_number The note number. + * @param aftertouch The aftertouch value. + * + * @par Thread-safety constraints + * - @b RT: the function must be invoked from the Real-time thread + */ +SFIZZ_EXPORTED_API void sfizz_send_poly_aftertouch(sfizz_synth_t* synth, int delay, int note_number, char aftertouch); + /** * @brief Send a tempo event. * @since 0.2.0 @@ -823,7 +839,7 @@ SFIZZ_EXPORTED_API void sfizz_clear_external_definitions(sfizz_synth_t* synth); * @brief Index out of bound error for the requested CC/key label. * @since 0.4.0 */ -#define SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX -1 +#define SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX (-1) /** * @brief Get the number of key labels registered in the current sfz file. diff --git a/src/sfizz.hpp b/src/sfizz.hpp index e7b0346c3..382be9aca 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -425,7 +425,7 @@ class SFIZZ_EXPORTED_API Sfizz void pitchWheel(int delay, int pitch) noexcept; /** - * @brief Send a aftertouch event to the synth. (CURRENTLY UNIMPLEMENTED) + * @brief Send an aftertouch event to the synth. * * @since 0.2.0 * @@ -438,6 +438,22 @@ class SFIZZ_EXPORTED_API Sfizz */ void aftertouch(int delay, uint8_t aftertouch) noexcept; + /** + * @brief Send a polyphonic aftertouch event to the synth. This feature is + * experimental and needs more testing in the internal engine. + * + * @since 0.6.0 + * + * @param delay the delay at which the event occurs; this should be lower + * than the size of the block in the next call to renderBlock(). + * @param noteNumber the note number. + * @param aftertouch the aftertouch value. + * + * @par Thread-safety constraints + * - @b RT: the function must be invoked from the Real-time thread + */ + void polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept; + /** * @brief Send a tempo event to the synth. * diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index d76ed822b..b4d900536 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -53,6 +53,8 @@ FloatSpec loVel { 0, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds }; FloatSpec hiVel { 127, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds }; FloatSpec loChannelAftertouch { 0, {0, 127}, kNormalizeMidi|kPermissiveBounds }; FloatSpec hiChannelAftertouch { 127, {0, 127}, kNormalizeMidi|kPermissiveBounds }; +FloatSpec loPolyAftertouch { 0, {0, 127}, kNormalizeMidi|kPermissiveBounds }; +FloatSpec hiPolyAftertouch { 127, {0, 127}, kNormalizeMidi|kPermissiveBounds }; FloatSpec loBend { -8191, {-8192.0f, 8191.0f}, kNormalizeBend|kPermissiveBounds }; FloatSpec hiBend { 8191, {-8192.0f, 8191.0f}, kNormalizeBend|kPermissiveBounds }; FloatSpec loNormalized { 0.0f, {0.0f, 1.0f}, kPermissiveBounds }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 9346dad8c..5ce7971be 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -163,6 +163,8 @@ namespace Default extern const OpcodeSpec hiBipolar; extern const OpcodeSpec loChannelAftertouch; extern const OpcodeSpec hiChannelAftertouch; + extern const OpcodeSpec loPolyAftertouch; + extern const OpcodeSpec hiPolyAftertouch; extern const OpcodeSpec ccNumber; extern const OpcodeSpec curveCC; extern const OpcodeSpec smoothCC; diff --git a/src/sfizz/Layer.cpp b/src/sfizz/Layer.cpp index 396aafb02..16a1b745c 100644 --- a/src/sfizz/Layer.cpp +++ b/src/sfizz/Layer.cpp @@ -59,7 +59,10 @@ bool Layer::registerNoteOn(int noteNumber, float velocity, float randValue) noex ((sequenceCounter_++ % region.sequenceLength) == region.sequencePosition - 1); } - if (!isSwitchedOn()) + const bool polyAftertouchActive = + region.polyAftertouchRange.containsWithEnd(midiState_.getPolyAftertouch(noteNumber)); + + if (!isSwitchedOn() || !polyAftertouchActive) return false; if (!region.triggerOnNote) @@ -83,7 +86,10 @@ bool Layer::registerNoteOff(int noteNumber, float velocity, float randValue) noe const Region& region = region_; - if (!isSwitchedOn()) + const bool polyAftertouchActive = + region.polyAftertouchRange.containsWithEnd(midiState_.getPolyAftertouch(noteNumber)); + + if (!isSwitchedOn() || !polyAftertouchActive) return false; if (!region.triggerOnNote) diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 73f7ea423..e077dfad1 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -73,8 +73,11 @@ void sfz::MidiState::flushEvents() noexcept events.resize(1); }; - for (auto& ccEvents : cc) - flushEventVector(ccEvents); + for (auto& events : ccEvents) + flushEventVector(events); + + for (auto& events: polyAftertouchEvents) + flushEventVector(events); flushEventVector(pitchEvents); flushEventVector(channelAftertouchEvents); @@ -88,8 +91,11 @@ void sfz::MidiState::setSamplesPerBlock(int samplesPerBlock) noexcept events.reserve(samplesPerBlock); }; this->samplesPerBlock = samplesPerBlock; - for (auto& ccEvents : cc) - updateEventBufferSize(ccEvents); + for (auto& events: ccEvents) + updateEventBufferSize(events); + + for (auto& events: polyAftertouchEvents) + updateEventBufferSize(events); updateEventBufferSize(pitchEvents); updateEventBufferSize(channelAftertouchEvents); @@ -147,21 +153,39 @@ void sfz::MidiState::channelAftertouchEvent(int delay, float aftertouch) noexcep insertEventInVector(channelAftertouchEvents, delay, aftertouch); } +void sfz::MidiState::polyAftertouchEvent(int delay, int noteNumber, float aftertouch) noexcept +{ + ASSERT(aftertouch >= 0.0f && aftertouch <= 1.0f); + if (noteNumber < 0 || noteNumber >= static_cast(polyAftertouchEvents.size())) + return; + + insertEventInVector(polyAftertouchEvents[noteNumber], delay, aftertouch); +} + float sfz::MidiState::getChannelAftertouch() const noexcept { ASSERT(channelAftertouchEvents.size() > 0); return channelAftertouchEvents.back().value; } +float sfz::MidiState::getPolyAftertouch(int noteNumber) const noexcept +{ + if (noteNumber < 0 || noteNumber > 127) + return 0.0f; + + ASSERT(polyAftertouchEvents[noteNumber].size() > 0); + return polyAftertouchEvents[noteNumber].back().value; +} + void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept { - insertEventInVector(cc[ccNumber], delay, ccValue); + insertEventInVector(ccEvents[ccNumber], delay, ccValue); } float sfz::MidiState::getCCValue(int ccNumber) const noexcept { ASSERT(ccNumber >= 0 && ccNumber < config::numCCs); - return cc[ccNumber].back().value; + return ccEvents[ccNumber].back().value; } void sfz::MidiState::reset() noexcept @@ -174,8 +198,11 @@ void sfz::MidiState::reset() noexcept events.push_back({ 0, 0.0f }); }; - for (auto& ccEvents : cc) - clearEvents(ccEvents); + for (auto& events : ccEvents) + clearEvents(events); + + for (auto& events : polyAftertouchEvents) + clearEvents(events); clearEvents(pitchEvents); clearEvents(channelAftertouchEvents); @@ -201,7 +228,7 @@ const sfz::EventVector& sfz::MidiState::getCCEvents(int ccIdx) const noexcept if (ccIdx < 0 || ccIdx >= config::numCCs) return nullEvent; - return cc[ccIdx]; + return ccEvents[ccIdx]; } const sfz::EventVector& sfz::MidiState::getPitchEvents() const noexcept @@ -213,3 +240,11 @@ const sfz::EventVector& sfz::MidiState::getChannelAftertouchEvents() const noexc { return channelAftertouchEvents; } + +const sfz::EventVector& sfz::MidiState::getPolyAftertouchEvents(int noteNumber) const noexcept +{ + if (noteNumber < 0 || noteNumber > 127) + return nullEvent; + + return polyAftertouchEvents[noteNumber]; +} diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index 93ec56e7e..3dab87bfb 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -113,6 +113,13 @@ class MidiState */ void channelAftertouchEvent(int delay, float aftertouch) noexcept; + /** + * @brief Register a channel aftertouch event + * + * @param aftertouch + */ + void polyAftertouchEvent(int delay, int noteNumber, float aftertouch) noexcept; + /** * @brief Get the channel aftertouch status @@ -120,6 +127,13 @@ class MidiState */ float getChannelAftertouch() const noexcept; + /** + * @brief Get the polyphonic aftertouch status + + * @return int + */ + float getPolyAftertouch(int noteNumber) const noexcept; + /** * @brief Register a CC event * @@ -172,6 +186,7 @@ class MidiState void resetAllControllers(int delay) noexcept; const EventVector& getCCEvents(int ccIdx) const noexcept; + const EventVector& getPolyAftertouchEvents(int noteNumber) const noexcept; const EventVector& getPitchEvents() const noexcept; const EventVector& getChannelAftertouchEvents() const noexcept; @@ -230,7 +245,7 @@ class MidiState * @brief Current known values for the CCs. * */ - std::array cc; + std::array ccEvents; /** * @brief Null event @@ -248,6 +263,11 @@ class MidiState */ EventVector channelAftertouchEvents; + /** + * @brief Polyphonic aftertouch status. + */ + std::array polyAftertouchEvents; + float sampleRate { config::defaultSampleRate }; int samplesPerBlock { config::defaultSamplesPerBlock }; float alternate { 0.0f }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index a32d6754a..5e1c0094f 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -348,6 +348,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("hichanaft"): aftertouchRange.setEnd(opcode.read(Default::hiChannelAftertouch)); break; + case hash("lopolyaft"): + polyAftertouchRange.setStart(opcode.read(Default::loPolyAftertouch)); + break; + case hash("hipolyaft"): + polyAftertouchRange.setEnd(opcode.read(Default::hiPolyAftertouch)); + break; case hash("lobpm"): bpmRange.setStart(opcode.read(Default::loBPM)); break; @@ -926,7 +932,9 @@ bool sfz::Region::parseLFOOpcode(const Opcode& opcode, LFODescription& lfo) = opcode.read(depthModSpec); break; case_any_lfo("depthpolyaft"): // NOLINT bugprone-branch-clone - // TODO(jpc) LFO v1 + getOrCreateConnection(sourceKey, targetKey).sourceDepthMod = sourceDepthKey; + getOrCreateConnection(ModKey::createNXYZ(ModId::PolyAftertouch, id), sourceDepthKey).sourceDepth + = opcode.read(depthModSpec); break; case_any_lfo("fade"): lfo.fade = opcode.read(Default::lfoFade); @@ -942,7 +950,8 @@ bool sfz::Region::parseLFOOpcode(const Opcode& opcode, LFODescription& lfo) = opcode.read(Default::lfoFreqMod); break; case_any_lfo("freqpolyaft"): // NOLINT bugprone-branch-clone - // TODO(jpc) LFO v1 + getOrCreateConnection(ModKey::createNXYZ(ModId::PolyAftertouch, id), lfo.freqKey).sourceDepth + = opcode.read(Default::lfoFreqMod); break; // sfizz extension diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 0cfdb921c..757971cbb 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -362,6 +362,7 @@ struct Region { // Region logic: internal conditions UncheckedRange aftertouchRange { Default::loChannelAftertouch, Default::hiChannelAftertouch }; // hichanaft and lochanaft + UncheckedRange polyAftertouchRange { Default::loPolyAftertouch, Default::hiPolyAftertouch }; // hipolyaft and lopolyaft UncheckedRange bpmRange { Default::loBPM, Default::hiBPM }; // hibpm and lobpm UncheckedRange randRange { Default::loNormalized, Default::hiNormalized }; // hirand and lorand uint8_t sequenceLength { Default::sequence }; // seq_length diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 1febb49c1..7268ecd8c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -64,6 +64,7 @@ Synth::Impl::Impl() genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceManager_)); genADSREnvelope_.reset(new ADSREnvelopeSource(voiceManager_, resources_.midiState)); genChannelAftertouch_.reset(new ChannelAftertouchSource(voiceManager_, resources_.midiState)); + genPolyAftertouch_.reset(new PolyAftertouchSource(voiceManager_, resources_.midiState)); } Synth::Impl::~Impl() @@ -1337,6 +1338,26 @@ void Synth::hdAftertouch(int delay, float normAftertouch) noexcept impl.performHdcc(delay, ExtendedCCs::channelAftertouch, normAftertouch, false); } +void Synth::polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept +{ + const float normalizedAftertouch = normalize7Bits(aftertouch); + hdPolyAftertouch(delay, noteNumber, normalizedAftertouch); +} + +void Synth::hdPolyAftertouch(int delay, int noteNumber, float normAftertouch) noexcept +{ + Impl& impl = *impl_; + ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration }; + + impl.resources_.midiState.polyAftertouchEvent(delay, noteNumber, normAftertouch); + + for (auto& voice : impl.voiceManager_) + voice.registerPolyAftertouch(delay, noteNumber, normAftertouch); + + // Note information is lost on this CC + impl.performHdcc(delay, ExtendedCCs::polyphonicAftertouch, normAftertouch, false); +} + void Synth::tempo(int delay, float secondsPerBeat) noexcept { Impl& impl = *impl_; @@ -1721,6 +1742,9 @@ void Synth::Impl::setupModMatrix() case ModId::ChannelAftertouch: gen = genChannelAftertouch_.get(); break; + case ModId::PolyAftertouch: + gen = genPolyAftertouch_.get(); + break; default: DBG("[sfizz] Have unknown type of source generator"); break; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 5bdf3ba4e..5e8a95fa2 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -421,6 +421,22 @@ class Synth final { * @param secondsPerQuarter the new period of the quarter note */ void tempo(int delay, float secondsPerQuarter) noexcept; + /** + * @brief Send a polyphonic aftertouch event to the synth + * + * @param delay + * @param noteNumber + * @param normAftertouch + */ + void polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept; + /** + * @brief Send a polyphonic aftertouch event to the synth + * + * @param delay + * @param noteNumber + * @param normAftertouch + */ + void hdPolyAftertouch(int delay, int noteNumber, float normAftertouch) noexcept; /** * @brief Send the time signature. * diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 5cead784d..81fd6b494 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -446,6 +446,14 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive(delay, path, "ff", args); } break; + MATCH("/region&/polyaft_range", "") { + GET_REGION_OR_BREAK(indices[0]) + sfizz_arg_t args[2]; + args[0].f = region.polyAftertouchRange.getStart(); + args[1].f = region.polyAftertouchRange.getEnd(); + client.receive(delay, path, "ff", args); + } break; + MATCH("/region&/bpm_range", "") { GET_REGION_OR_BREAK(indices[0]) sfizz_arg_t args[2]; diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 1fafde390..12ca37364 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -11,6 +11,7 @@ #include "modulations/sources/Controller.h" #include "modulations/sources/FlexEnvelope.h" #include "modulations/sources/ChannelAftertouch.h" +#include "modulations/sources/PolyAftertouch.h" #include "modulations/sources/LFO.h" namespace sfz { @@ -303,6 +304,7 @@ struct Synth::Impl final: public Parser::Listener { std::unique_ptr genFlexEnvelope_; std::unique_ptr genADSREnvelope_; std::unique_ptr genChannelAftertouch_; + std::unique_ptr genPolyAftertouch_; // Settings per voice struct { diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 0a3f8c747..3e49b9f59 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -16,6 +16,7 @@ #include "LFO.h" #include "MathHelpers.h" #include "ModifierHelpers.h" +#include "TriggerEvent.h" #include "modulations/ModId.h" #include "modulations/ModKey.h" #include "modulations/ModMatrix.h" @@ -658,13 +659,28 @@ void Voice::registerPitchWheel(int delay, float pitch) noexcept UNUSED(pitch); } -void Voice::registerAftertouch(int delay, uint8_t aftertouch) noexcept +void Voice::registerAftertouch(int delay, float aftertouch) noexcept { // TODO UNUSED(delay); UNUSED(aftertouch); } +void Voice::registerPolyAftertouch(int delay, int noteNumber, float aftertouch) noexcept +{ + Impl& impl = *impl_; + if (impl.state_ != State::playing) + return; + + if (!(impl.triggerEvent_.type == TriggerEventType::NoteOn || impl.triggerEvent_.type == TriggerEventType::NoteOff) + || impl.triggerEvent_.number != noteNumber) + return; + + // TODO + UNUSED(delay); + UNUSED(aftertouch); +} + void Voice::registerTempo(int delay, float secondsPerQuarter) noexcept { // TODO diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 51b04d02b..582425521 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -153,7 +153,17 @@ class Voice { * @param delay * @param aftertouch */ - void registerAftertouch(int delay, uint8_t aftertouch) noexcept; + void registerAftertouch(int delay, float aftertouch) noexcept; + + /** + * @brief Register a polyphonic aftertouch event; for now this does nothing + * + * @param delay + * @param noteNumber + * @param aftertouch + */ + void registerPolyAftertouch(int delay, int noteNumber, float aftertouch) noexcept; + /** * @brief Register a tempo event; for now this does nothing * diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index 2d57a69bb..7611b24ef 100644 --- a/src/sfizz/modulations/ModId.cpp +++ b/src/sfizz/modulations/ModId.cpp @@ -44,6 +44,8 @@ int ModIds::flags(ModId id) noexcept return kModIsPerVoice; case ModId::ChannelAftertouch: return kModIsPerCycle; + case ModId::PolyAftertouch: + return kModIsPerVoice; case ModId::PerVoiceController: return kModIsPerVoice; diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index b6a3e130b..d35a0fe64 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -30,6 +30,7 @@ enum class ModId : int { PitchEG, FilEG, ChannelAftertouch, + PolyAftertouch, PerVoiceController, _SourcesEnd, diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 107b0b349..515927a1e 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -113,6 +113,8 @@ std::string ModKey::toString() const return absl::StrCat("FilterEG {", region_.number(), "}"); case ModId::ChannelAftertouch: return absl::StrCat("ChannelAftertouch"); + case ModId::PolyAftertouch: + return absl::StrCat("PolyAftertouch"); case ModId::PerVoiceController: return absl::StrCat("PerVoiceController ", params_.cc, " {curve=", params_.curve, ", smooth=", params_.smooth, diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp index b94f9c623..f6b16fb03 100644 --- a/src/sfizz/modulations/sources/Controller.cpp +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -106,6 +106,16 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceI }; switch(p.cc) { + case ExtendedCCs::polyphonicAftertouch: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = + voice && voice->getTriggerEvent().type == TriggerEventType::NoteOn ? + impl_->res_->midiState.getPolyAftertouch(voice->getTriggerEvent().number) : 0.0f; + + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } case ExtendedCCs::noteOnVelocity: { const auto voice = impl_->voiceManager_->getVoiceById(voiceId); const float fillValue = diff --git a/src/sfizz/modulations/sources/PolyAftertouch.cpp b/src/sfizz/modulations/sources/PolyAftertouch.cpp new file mode 100644 index 000000000..ce91f7bbb --- /dev/null +++ b/src/sfizz/modulations/sources/PolyAftertouch.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "PolyAftertouch.h" +#include "../../ModifierHelpers.h" +#include "../../ADSREnvelope.h" + +namespace sfz { + +PolyAftertouchSource::PolyAftertouchSource(VoiceManager& manager, MidiState& state) + : midiState_(state), manager_(manager) +{ + +} + +void PolyAftertouchSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + UNUSED(sourceKey); + UNUSED(voiceId); + UNUSED(delay); +} + +void PolyAftertouchSource::release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) +{ + UNUSED(sourceKey); + UNUSED(voiceId); + UNUSED(delay); +} + +void PolyAftertouchSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) +{ + UNUSED(sourceKey); + Voice* voice = manager_.getVoiceById(voiceId); + if (!voice || voice->getTriggerEvent().type == TriggerEventType::CC) { + fill(buffer, 0.0f); + return; + } + + const int noteNumber = voice->getTriggerEvent().number; + + const EventVector& events = midiState_.getPolyAftertouchEvents(noteNumber); + linearEnvelope(events, buffer, [](float x) { return x; }); +} + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/PolyAftertouch.h b/src/sfizz/modulations/sources/PolyAftertouch.h new file mode 100644 index 000000000..c70ea360b --- /dev/null +++ b/src/sfizz/modulations/sources/PolyAftertouch.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 PolyAftertouchSource : public ModGenerator { +public: + explicit PolyAftertouchSource(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: + MidiState& midiState_; + VoiceManager& manager_; +}; + +} // namespace sfz diff --git a/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index 8a6d63431..51391abb1 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -185,6 +185,11 @@ void sfz::Sfizz::aftertouch(int delay, uint8_t aftertouch) noexcept synth->synth.aftertouch(delay, aftertouch); } +void sfz::Sfizz::polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept +{ + synth->synth.polyAftertouch(delay, noteNumber, aftertouch); +} + void sfz::Sfizz::tempo(int delay, float secondsPerBeat) noexcept { synth->synth.tempo(delay, secondsPerBeat); diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index ab3057341..73284eb2a 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -142,6 +142,10 @@ void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, char aftertouch) { synth->synth.aftertouch(delay, aftertouch); } +void sfizz_send_poly_aftertouch(sfizz_synth_t* synth, int delay, int note_number, char aftertouch) +{ + synth->synth.polyAftertouch(delay, note_number, aftertouch); +} void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float seconds_per_quarter) { synth->synth.tempo(delay, seconds_per_quarter); diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index bd701ae60..48ede30b1 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -430,7 +430,6 @@ TEST_CASE("[Modulations] LFO v1 aftertouch connections") }, 3)); } - TEST_CASE("[Modulations] LFO v1 aftertouch frequency connections") { sfz::Synth synth; @@ -451,6 +450,46 @@ TEST_CASE("[Modulations] LFO v1 aftertouch frequency connections") }, 3)); } +TEST_CASE("[Modulations] LFO v1 poly aftertouch connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine amplfo_depthpolyaft=10 + sample=*sine pitchlfo_depthpolyaft=1200 + sample=*sine fillfo_depthpolyaft=-3600 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createDefaultGraph({ + R"("PolyAftertouch" -> "AmplitudeLFODepth {0}")", + R"("PolyAftertouch" -> "PitchLFODepth {1}")", + R"("PolyAftertouch" -> "FilterLFODepth {2}")", + R"("AmplitudeLFO {0}" -> "Volume {0}")", + R"("PitchLFO {1}" -> "Pitch {1}")", + R"("FilterLFO {2}" -> "FilterCutoff {2, N=1}")", + }, 3)); +} + +TEST_CASE("[Modulations] LFO v1 poly aftertouch frequency connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine amplfo_freqpolyaft=10 + sample=*sine pitchlfo_freqpolyaft=1200 + sample=*sine fillfo_freqpolyaft=-3600 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createDefaultGraph({ + R"("PolyAftertouch" -> "AmplitudeLFOFrequency {0}")", + R"("PolyAftertouch" -> "PitchLFOFrequency {1}")", + R"("PolyAftertouch" -> "FilterLFOFrequency {2}")", + R"("AmplitudeLFO {0}" -> "Volume {0}")", + R"("PitchLFO {1}" -> "Pitch {1}")", + R"("FilterLFO {2}" -> "FilterCutoff {2, N=1}")", + }, 3)); +} + TEST_CASE("[Modulations] EG v1 CC connections") { sfz::Synth synth; diff --git a/tests/RegionActivationT.cpp b/tests/RegionActivationT.cpp index 846711738..d62e1ef1b 100644 --- a/tests/RegionActivationT.cpp +++ b/tests/RegionActivationT.cpp @@ -537,3 +537,47 @@ TEST_CASE("[Keyswitches] Multiple sw_default, in region") REQUIRE(!synth.getLayerView(1)->isSwitchedOn()); } +TEST_CASE("[Region activation] Polyphonic aftertouch") +{ + sfz::Synth synth; + SECTION("Basic sequence, note on") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyaft.sfz", R"( + sample=*saw lokey=48 hikey=60 + lopolyaft=50 hipolyaft=100 sample=*sine lokey=36 hikey=47 + )"); + synth.noteOn(0, 50, 100); + REQUIRE( synth.getNumActiveVoices() == 1); + synth.noteOn(1, 40, 100); + REQUIRE( synth.getNumActiveVoices() == 1); // no notes playing + synth.polyAftertouch(2, 40, 80); + synth.noteOn(3, 40, 100); + REQUIRE( synth.getNumActiveVoices() == 2); + } + + SECTION("Basic sequence, note off, no polyaft set") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyaft.sfz", R"( + sample=*saw + lopolyaft=50 hipolyaft=100 sample=*sine trigger=release + )"); + synth.noteOn(0, 50, 100); + REQUIRE( synth.getNumActiveVoices() == 1); + synth.noteOff(1, 50, 0); + REQUIRE( synth.getNumActiveVoices() == 1); // no note off playing + } + + SECTION("Basic sequence, note off") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyaft.sfz", R"( + sample=*saw + lopolyaft=50 hipolyaft=100 sample=*sine trigger=release + )"); + synth.noteOn(0, 50, 100); + REQUIRE( synth.getNumActiveVoices() == 1); + synth.polyAftertouch(2, 50, 80); + synth.noteOff(3, 50, 0); + REQUIRE( synth.getNumActiveVoices() == 2); // no note off playing + } +} + diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 3f0699007..9628d3a68 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -922,6 +922,34 @@ TEST_CASE("[Values] Aftertouch range") REQUIRE(messageList == expected); } +TEST_CASE("[Values] Polyaftertouch range") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav lopolyaft=34 hipolyaft=60 + sample=kick.wav lopolyaft=-3 hipolyaft=60 + sample=kick.wav lopolyaft=20 hipolyaft=-1 + sample=kick.wav lopolyaft=20 hipolyaft=10 + )"); + synth.dispatchMessage(client, 0, "/region0/polyaft_range", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/polyaft_range", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/polyaft_range", "", nullptr); + synth.dispatchMessage(client, 0, "/region3/polyaft_range", "", nullptr); + synth.dispatchMessage(client, 0, "/region4/polyaft_range", "", nullptr); + std::vector expected { + "/region0/polyaft_range,ff : { 0, 1 }", + "/region1/polyaft_range,ff : { 0.267717, 0.472441 }", + "/region2/polyaft_range,ff : { -0.023622, 0.472441 }", + "/region3/polyaft_range,ff : { 0.15748, -0.00787402 }", + "/region4/polyaft_range,ff : { 0.15748, 0.0787402 }", + }; + REQUIRE(messageList == expected); +} + TEST_CASE("[Values] BPM range") { Synth synth;