diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 938fba1ed..b5979a0cf 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -23,6 +23,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex noteOnTimes[noteNumber] = internalClock + static_cast(delay); lastNotePlayed = noteNumber; activeNotes++; + alternate = alternate == 0.0f ? 1.0f : 0.0f; } } diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index f9766c810..524b14724 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -165,6 +165,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: /** @@ -227,6 +234,7 @@ class MidiState float sampleRate { config::defaultSampleRate }; int samplesPerBlock { config::defaultSamplesPerBlock }; + float alternate { 0.0f }; unsigned internalClock { 0 }; }; } diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index e066e4301..1b1e229d6 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1486,7 +1486,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 654388cdc..4a61f3374 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1642,6 +1642,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 94aba5bb1..2674483fa 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -195,6 +195,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; @@ -283,6 +289,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) @@ -358,6 +368,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(Region* region, int delay, const TriggerEvent& event) noexcept { Impl& impl = *impl_; @@ -376,6 +400,8 @@ bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe 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 eae11f4f5..a8597b365 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -18,6 +18,14 @@ namespace sfz { enum InterpolatorModel : int; class LFO; class FlexEnvelope; + +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 @@ -383,6 +391,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 c207b59ff..97517c8f8 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 cef95c506..1b8bae844 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 6abc40c5f..5f59cef2e 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 f94e14523..fb6f3f48a 100644 --- a/src/sfizz/modulations/sources/Controller.cpp +++ b/src/sfizz/modulations/sources/Controller.cpp @@ -87,8 +87,6 @@ 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); @@ -96,7 +94,22 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceI bool canShortcut = false; auto transformValue = [p, &curve](float x) { - return curve.evalNormalized(x); + // Otherwise it clamps the bipolar values + if (p.curve != 0) + x = curve.evalNormalized(x); + + return x; + }; + + auto extendedCCTransform = [p, &curve](float x) { + // Otherwise it clamps the bipolar values + if (p.curve != 0) + x = curve.evalNormalized(x); + + if (p.step > 0.0f) + return std::trunc(x / p.step) * p.step; + + return x; }; switch(p.cc) { @@ -106,11 +119,7 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceI voice && voice->getTriggerEvent().type == TriggerEventType::NoteOn ? voice->getTriggerEvent().value : 0.0f; - if (p.step > 0.0f) - sfz::fill(buffer, std::trunc(transformValue(fillValue) / p.step) * p.step); - else - sfz::fill(buffer, transformValue(fillValue)); - + sfz::fill(buffer, extendedCCTransform(fillValue)); canShortcut = true; break; } @@ -120,11 +129,47 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId voiceI voice && voice->getTriggerEvent().type == TriggerEventType::NoteOff ? voice->getTriggerEvent().value : 0.0f; - if (p.step > 0.0f) - sfz::fill(buffer, std::trunc(transformValue(fillValue) / p.step) * p.step); - else - sfz::fill(buffer, transformValue(fillValue)); + sfz::fill(buffer, extendedCCTransform(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, extendedCCTransform(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, extendedCCTransform(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, extendedCCTransform(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, extendedCCTransform(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, extendedCCTransform(fillValue)); canShortcut = true; break; } diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 98fbec577..23bd7933d 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -408,3 +408,33 @@ TEST_CASE("[Modulations] EG v1 CC connections") R"("FilterEG {1}" -> "FilterCutoff {1, N=1}")", }, 2)); } + +TEST_CASE("[Modulations] Extended CCs 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)); +}