From b3a73814d5385b46416d2530657f5edf2879a670 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Thu, 25 Mar 2021 12:56:13 +0100 Subject: [PATCH] Support the extended CCs of SFZv2 except poly aftertouch Poly aftertouch can be added in the future in a similar way --- src/sfizz/MidiState.cpp | 2 +- src/sfizz/MidiState.h | 8 ++ src/sfizz/ModifierHelpers.h | 8 +- src/sfizz/Region.cpp | 16 +++- src/sfizz/Synth.cpp | 5 +- src/sfizz/Voice.cpp | 26 ++++++ src/sfizz/Voice.h | 15 ++++ src/sfizz/modulations/ModId.cpp | 2 + src/sfizz/modulations/ModId.h | 2 +- src/sfizz/modulations/ModKey.cpp | 4 + src/sfizz/modulations/sources/Controller.cpp | 90 ++++++++++++++++++-- src/sfizz/modulations/sources/Controller.h | 3 +- tests/ModulationsT.cpp | 30 +++++++ 13 files changed, 196 insertions(+), 15 deletions(-) diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 6da30f3fc..6bd07b62b 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -24,6 +24,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex lastNotePlayed = noteNumber; activeNotes++; noteStates[noteNumber] = true; + alternate = alternate == 0.0f ? 1.0f : 0.0f; } } @@ -154,7 +155,6 @@ float sfz::MidiState::getChannelAftertouch() const noexcept void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept { - ASSERT(ccValue >= 0.0 && ccValue <= 1.0); insertEventInVector(cc[ccNumber], delay, ccValue); } diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index bb42f09af..93ec56e7e 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -175,6 +175,13 @@ class MidiState const EventVector& getPitchEvents() const noexcept; const EventVector& getChannelAftertouchEvents() const noexcept; + /** + * @brief Get the alternate state value, for extended CC 137 + * + * @return float + */ + float getAlternateState() const noexcept { return alternate; } + private: /** @@ -243,6 +250,7 @@ class MidiState float sampleRate { config::defaultSampleRate }; int samplesPerBlock { config::defaultSamplesPerBlock }; + float alternate { 0.0f }; unsigned internalClock { 0 }; }; } diff --git a/src/sfizz/ModifierHelpers.h b/src/sfizz/ModifierHelpers.h index 6a2e26b54..e233009db 100644 --- a/src/sfizz/ModifierHelpers.h +++ b/src/sfizz/ModifierHelpers.h @@ -73,6 +73,7 @@ void linearEnvelope(const EventVector& events, absl::Span envelope, F&& l { ASSERT(events.size() > 0); ASSERT(events[0].delay == 0); + if (envelope.size() == 0) return; @@ -103,7 +104,11 @@ void linearEnvelope(const EventVector& events, absl::Span envelope, F&& l { ASSERT(events.size() > 0); ASSERT(events[0].delay == 0); - ASSERT(step != 0.0); + + if (step == 0.0f) { + linearEnvelope(events, envelope, std::forward(lambda)); + return; + } if (envelope.size() == 0) return; @@ -155,6 +160,7 @@ void multiplicativeEnvelope(const EventVector& events, absl::Span envelop if (envelope.size() == 0) return; + const auto maxDelay = static_cast(envelope.size() - 1); auto lastValue = lambda(events[0].value); diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 9072ccd92..ac395b038 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1526,7 +1526,21 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, OpcodeSpec spec, assert(false); break; } - conn->source = ModKey(ModId::Controller, {}, p); + + switch (p.cc) { + case ExtendedCCs::noteOnVelocity: // fallthrough + case ExtendedCCs::noteOffVelocity: // fallthrough + case ExtendedCCs::keyboardNoteNumber: // fallthrough + case ExtendedCCs::keyboardNoteGate: // fallthrough + case ExtendedCCs::unipolarRandom: // fallthrough + case ExtendedCCs::bipolarRandom: // fallthrough + case ExtendedCCs::alternate: + conn->source = ModKey(ModId::PerVoiceController, id, p); + break; + default: + conn->source = ModKey(ModId::Controller, {}, p); + break; + } } return true; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index b96a6d616..681828055 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -59,7 +59,7 @@ Synth::Impl::Impl() resetVoices(config::numVoices); // modulation sources - genController_.reset(new ControllerSource(resources_)); + genController_.reset(new ControllerSource(resources_, voiceManager_)); genLFO_.reset(new LFOSource(voiceManager_)); genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceManager_)); genADSREnvelope_.reset(new ADSREnvelopeSource(voiceManager_, resources_.midiState)); @@ -1314,6 +1314,8 @@ void Synth::pitchWheel(int delay, int pitch) noexcept for (auto& voice : impl.voiceManager_) { voice.registerPitchWheel(delay, normalizedPitch); } + + impl.performHdcc(delay, ExtendedCCs::pitchBend, normalizedPitch, false); } void Synth::aftertouch(int delay, uint8_t aftertouch) noexcept @@ -1704,6 +1706,7 @@ void Synth::Impl::setupModMatrix() switch (sourceKey.id()) { case ModId::Controller: + case ModId::PerVoiceController: gen = genController_.get(); break; case ModId::AmpLFO: diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a68210fde..10d5070cb 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -196,6 +196,12 @@ struct Voice::Impl */ void off(int delay, bool fast = false) noexcept; + /** + * @brief Setup the extended CC values for the voice + * + */ + void updateExtendedCCValues() noexcept; + const NumericId id_; StateListener* stateListener_ = nullptr; @@ -288,6 +294,10 @@ struct Voice::Impl bool followPower_ { false }; PowerFollower powerFollower_; + + ExtendedCCValues extendedCCValues_; + fast_real_distribution unipolarDist { 0.0f, 1.0f }; + fast_real_distribution bipolarDist { -1.0f, 1.0f }; }; Voice::Voice(int voiceNumber, Resources& resources) @@ -363,6 +373,20 @@ Voice::Impl::Impl(int voiceNumber, Resources& resources) getSCurve(); } +const ExtendedCCValues& Voice::getExtendedCCValues() const noexcept +{ + Impl& impl = *impl_; + return impl.extendedCCValues_; +} + +void Voice::Impl::updateExtendedCCValues() noexcept +{ + extendedCCValues_.unipolar = unipolarDist(Random::randomGenerator); + extendedCCValues_.bipolar = bipolarDist(Random::randomGenerator); + extendedCCValues_.alternate = resources_.midiState.getAlternateState(); + extendedCCValues_.noteGate = resources_.midiState.getActiveNotes() > 0 ? 1.0f : 0.0f; +} + bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexcept { Impl& impl = *impl_; @@ -384,6 +408,8 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc impl.switchState(State::playing); + impl.updateExtendedCCValues(); + ASSERT(delay >= 0); if (delay < 0) delay = 0; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index d6412424e..51b04d02b 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -19,6 +19,14 @@ enum InterpolatorModel : int; class LFO; class FlexEnvelope; struct Layer; + +struct ExtendedCCValues { + float unipolar {}; + float bipolar {}; + float noteGate {}; + float alternate {}; +}; + /** * @brief The SFZ voice are the polyphony holders. They get activated by the synth * and tasked to play a given region until the end, stopping on note-offs, off-groups @@ -384,6 +392,13 @@ class Voice { */ const TriggerEvent& getTriggerEvent(); + /** + * @brief Get the extended CC values + * + * @return const ExtendedCCValues& + */ + const ExtendedCCValues& getExtendedCCValues() const noexcept; + public: /** * @brief Check if the voice already belongs to a sister ring diff --git a/src/sfizz/modulations/ModId.cpp b/src/sfizz/modulations/ModId.cpp index a293b2677..cc08a16ad 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::PerVoiceController: + return kModIsPerVoice; // targets case ModId::MasterAmplitude: diff --git a/src/sfizz/modulations/ModId.h b/src/sfizz/modulations/ModId.h index cd64b130b..251ba9e3d 100644 --- a/src/sfizz/modulations/ModId.h +++ b/src/sfizz/modulations/ModId.h @@ -30,7 +30,7 @@ enum class ModId : int { PitchEG, FilEG, ChannelAftertouch, - + PerVoiceController, _SourcesEnd, //-------------------------------------------------------------------------- diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index 7f73a25e0..3fef944f1 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -113,6 +113,10 @@ std::string ModKey::toString() const return absl::StrCat("FilterEG {", region_.number(), "}"); case ModId::ChannelAftertouch: return absl::StrCat("ChannelAftertouch"); + case ModId::PerVoiceController: + return absl::StrCat("PerVoiceController ", params_.cc, + " {curve=", params_.curve, ", smooth=", params_.smooth, + ", step=", params_.step, ", region=", region_.number(), "}"); case ModId::MasterAmplitude: return absl::StrCat("MasterAmplitude {", region_.number(), "}"); diff --git a/src/sfizz/modulations/sources/Controller.cpp b/src/sfizz/modulations/sources/Controller.cpp index 3b2e7470c..b94f9c623 100644 --- a/src/sfizz/modulations/sources/Controller.cpp +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -19,13 +19,15 @@ struct ControllerSource::Impl { float getLastTransformedValue(uint16_t cc, uint8_t curve) const noexcept; double sampleRate_ = config::defaultSampleRate; Resources* res_ = nullptr; + VoiceManager* voiceManager_ = nullptr; absl::flat_hash_map smoother_; }; -ControllerSource::ControllerSource(Resources& res) +ControllerSource::ControllerSource(Resources& res, VoiceManager& manager) : impl_(new Impl) { impl_->res_ = &res; + impl_->voiceManager_ = &manager; } ControllerSource::~ControllerSource() @@ -85,27 +87,97 @@ void ControllerSource::init(const ModKey& sourceKey, NumericId voiceId, u void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) { - (void)voiceId; - const ModKey::Parameters p = sourceKey.parameters(); const Resources& res = *impl_->res_; const Curve& curve = res.curves.getCurve(p.curve); const MidiState& ms = res.midiState; - const EventVector& events = ms.getCCEvents(p.cc); + bool canShortcut = false; auto transformValue = [p, &curve](float x) { return curve.evalNormalized(x); }; - if (p.step > 0.0f) - linearEnvelope(events, buffer, transformValue, p.step); - else - linearEnvelope(events, buffer, transformValue); + // Ignore the eventual curve for the extended CCs + auto quantize = [p](float x) { + if (p.step > 0.0f) + return std::trunc(x / p.step) * p.step; + + return x; + }; + + switch(p.cc) { + case ExtendedCCs::noteOnVelocity: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = + voice && voice->getTriggerEvent().type == TriggerEventType::NoteOn ? + voice->getTriggerEvent().value : 0.0f; + + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } + case ExtendedCCs::noteOffVelocity: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = + voice && voice->getTriggerEvent().type == TriggerEventType::NoteOff ? + voice->getTriggerEvent().value : 0.0f; + + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } + case ExtendedCCs::keyboardNoteNumber: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = voice ? normalize7Bits(voice->getTriggerEvent().number) : 0.0f; + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } + case ExtendedCCs::keyboardNoteGate: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = voice ? voice->getExtendedCCValues().noteGate : 0.0f; + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } + case ExtendedCCs::unipolarRandom: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = voice ? voice->getExtendedCCValues().unipolar : 0.0f; + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } + case ExtendedCCs::bipolarRandom: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = voice ? voice->getExtendedCCValues().bipolar : 0.0f; + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } + case ExtendedCCs::alternate: { + const auto voice = impl_->voiceManager_->getVoiceById(voiceId); + const float fillValue = voice ? voice->getExtendedCCValues().alternate : 0.0f; + sfz::fill(buffer, quantize(fillValue)); + canShortcut = true; + break; + } + case ExtendedCCs::pitchBend: // fallthrough + case ExtendedCCs::channelAftertouch: { + const EventVector& events = ms.getCCEvents(p.cc); + linearEnvelope(events, buffer, [](float x) { return x; }, p.step); + canShortcut = events.size() == 1; + break; + } + default: { + const EventVector& events = ms.getCCEvents(p.cc); + linearEnvelope(events, buffer, transformValue, p.step); + canShortcut = events.size() == 1; + } + } auto it = impl_->smoother_.find(sourceKey); if (it != impl_->smoother_.end()) { Smoother& s = it->second; - bool canShortcut = events.size() == 1; s.process(buffer, buffer, canShortcut); } } diff --git a/src/sfizz/modulations/sources/Controller.h b/src/sfizz/modulations/sources/Controller.h index 829f03332..2736bb5be 100644 --- a/src/sfizz/modulations/sources/Controller.h +++ b/src/sfizz/modulations/sources/Controller.h @@ -6,6 +6,7 @@ #pragma once #include "../ModGenerator.h" +#include "../../VoiceManager.h" #include namespace sfz { @@ -14,7 +15,7 @@ struct Resources; class ControllerSource : public ModGenerator { public: - explicit ControllerSource(Resources& res); + explicit ControllerSource(Resources& res, VoiceManager& manager); ~ControllerSource(); void setSampleRate(double sampleRate) override; void setSamplesPerBlock(unsigned count) override; diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 05b834171..48179b660 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -410,6 +410,36 @@ TEST_CASE("[Modulations] EG v1 CC connections") } TEST_CASE("[Modulations] LFO CC connections") +{ + sfz::Synth synth; + synth.loadSfzString("/modulation.sfz", R"( + sample=*sine + pitch_oncc128=1200 + pitch_oncc129=1200 + pitch_oncc131=1200 + pitch_oncc132=1200 + pitch_oncc133=1200 + pitch_oncc134=1200 + pitch_oncc135=1200 + pitch_oncc136=1200 + pitch_oncc137=1200 + )"); + + const std::string graph = synth.getResources().modMatrix.toDotGraph(); + REQUIRE(graph == createDefaultGraph({ + R"("Controller 128 {curve=0, smooth=0, step=0}" -> "Pitch {0}")", + R"("Controller 129 {curve=0, smooth=0, step=0}" -> "Pitch {0}")", + R"("PerVoiceController 131 {curve=0, smooth=0, step=0, region=0}" -> "Pitch {0}")", + R"("PerVoiceController 132 {curve=0, smooth=0, step=0, region=0}" -> "Pitch {0}")", + R"("PerVoiceController 133 {curve=0, smooth=0, step=0, region=0}" -> "Pitch {0}")", + R"("PerVoiceController 134 {curve=0, smooth=0, step=0, region=0}" -> "Pitch {0}")", + R"("PerVoiceController 135 {curve=0, smooth=0, step=0, region=0}" -> "Pitch {0}")", + R"("PerVoiceController 136 {curve=0, smooth=0, step=0, region=0}" -> "Pitch {0}")", + R"("PerVoiceController 137 {curve=0, smooth=0, step=0, region=0}" -> "Pitch {0}")", + }, 1)); +} + +TEST_CASE("[Modulations] Extended CCs connections") { sfz::Synth synth; synth.loadSfzString("/modulation.sfz", R"(