diff --git a/framework/audio/common/automatablevalue.h b/framework/audio/common/automatablevalue.h index 442e294ae0..0c275ec20d 100644 --- a/framework/audio/common/automatablevalue.h +++ b/framework/audio/common/automatablevalue.h @@ -26,13 +26,11 @@ #include "global/types/secs.h" #include "global/types/number.h" -#include "global/types/sharedmap.h" -#include "global/containers.h" #include "mpe/automationpoint.h" namespace muse::audio { -using AutomationEnvelope = SharedMap; +using AutomationEnvelope = mpe::AutomationCurve; //! NOTE: a parameter that is either a fixed value, or driven by an AutomationEnvelope over time template @@ -58,19 +56,7 @@ class AutomatableValue return T {}; } - auto it = findLessOrEqual(envelope, pos); - if (it == envelope.end()) { - //! NOTE: hold the first point's value backwards in time, matching standard envelope semantics - it = envelope.begin(); - } - - real_t normalized = it->second.outValue; - const auto next = std::next(it); - if (next != envelope.end()) { - const real_t t = static_cast(pos - it->first) / static_cast(next->first - it->first); - normalized = mpe::evaluateAt(next->second, it->second.outValue, t); - } - + const real_t normalized = mpe::evaluateCurveAt(envelope, pos); const T mapped = min + static_cast(normalized.raw()) * (max - min); return std::clamp(mapped, min, max); } diff --git a/framework/audio/common/rpc/rpcpacker.h b/framework/audio/common/rpc/rpcpacker.h index db031ffd4f..5bc8c7ceb3 100644 --- a/framework/audio/common/rpc/rpcpacker.h +++ b/framework/audio/common/rpc/rpcpacker.h @@ -127,8 +127,8 @@ void pack_custom(muse::msgpack::Packer& p, const muse::mpe::PlaybackEvent& value void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::PlaybackEvent& value); // AutomationPoint -void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::Bend& value); -void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::AutomationPoint::Bend& value); +void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::Ease& value); +void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::AutomationPoint::Ease& value); void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::ArrivalFromPrevious& value); void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::AutomationPoint::ArrivalFromPrevious& value); void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::ExplicitArrival& value); @@ -668,12 +668,12 @@ inline void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::PlaybackEvent& } } -inline void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::Bend& value) +inline void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::Ease& value) { p.process(value.t, value.value); } -inline void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::AutomationPoint::Bend& value) +inline void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::AutomationPoint::Ease& value) { p.process(value.t, value.value); } @@ -690,12 +690,12 @@ inline void unpack_custom(muse::msgpack::UnPacker&, muse::mpe::AutomationPoint:: inline void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::ExplicitArrival& value) { - p.process(value.value, value.bend); + p.process(value.value, value.ease); } inline void unpack_custom(muse::msgpack::UnPacker& p, muse::mpe::AutomationPoint::ExplicitArrival& value) { - p.process(value.value, value.bend); + p.process(value.value, value.ease); } inline void pack_custom(muse::msgpack::Packer& p, const muse::mpe::AutomationPoint::InValue& value) diff --git a/framework/audio/engine/internal/abstracteventsequencer.h b/framework/audio/engine/internal/abstracteventsequencer.h index d3262f330e..15ea0cb677 100644 --- a/framework/audio/engine/internal/abstracteventsequencer.h +++ b/framework/audio/engine/internal/abstracteventsequencer.h @@ -61,7 +61,7 @@ class AbstractEventSequencer : public async::Asyncable m_playbackData = data; m_playbackData.mainStream.onReceive(this, [this](const mpe::PlaybackEventsMap& events, - const mpe::DynamicLevelLayers& dynamics) { + const mpe::DynamicAutomationLayers& dynamics) { m_playbackData.originEvents = events; m_playbackData.dynamics = dynamics; m_shouldUpdateMainStreamEvents = true; @@ -72,13 +72,12 @@ class AbstractEventSequencer : public async::Asyncable }); m_playbackData.offStream.onReceive(this, [this](const mpe::PlaybackEventsMap& events, - const mpe::DynamicLevelLayers& dynamics, bool flush) { if (flush) { flushOffstream(); } - updateOffStreamEvents(events, dynamics); + updateOffStreamEvents(events); }); updateMainStreamEvents(data.originEvents, data.dynamics); @@ -165,19 +164,6 @@ class AbstractEventSequencer : public async::Asyncable m_onMainStreamFlushed = flushed; } - mpe::dynamic_level_t dynamicLevel(const msecs_t position) const - { - for (const auto& layer : m_playbackData.dynamics) { - const mpe::DynamicLevelMap& dynamics = layer.second; - auto it = muse::findLessOrEqual(dynamics, position); - if (it != dynamics.end()) { - return it->second; - } - } - - return mpe::dynamicLevelFromType(muse::mpe::DynamicType::Natural); - } - EventSequenceMap movePlaybackForward(const msecs_t nextMsecs) { ONLY_AUDIO_ENGINE_THREAD; @@ -211,8 +197,8 @@ class AbstractEventSequencer : public async::Asyncable } protected: - virtual void updateOffStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicLevelLayers& dynamics) = 0; - virtual void updateMainStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicLevelLayers& dynamics) = 0; + virtual void updateMainStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicAutomationLayers& dynamics) = 0; + virtual void updateOffStreamEvents(const mpe::PlaybackEventsMap& events) = 0; void resetAllIterators() { diff --git a/framework/audio/engine/internal/nodes/eventaudionode.cpp b/framework/audio/engine/internal/nodes/eventaudionode.cpp index 2543550129..26c8402931 100644 --- a/framework/audio/engine/internal/nodes/eventaudionode.cpp +++ b/framework/audio/engine/internal/nodes/eventaudionode.cpp @@ -40,7 +40,6 @@ EventAudioNode::EventAudioNode(TrackId trackId, const mpe::PlaybackData& playbac if (onOffStreamReceived) { m_playbackData.offStream.onReceive(this, [onOffStreamReceived](const PlaybackEventsMap&, - const DynamicLevelLayers&, bool) { onOffStreamReceived(); }); diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.cpp b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.cpp index 2680c35916..c4e637fa93 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.cpp +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.cpp @@ -36,21 +36,44 @@ static constexpr uint32_t CTRL_ON = 127; static constexpr uint32_t CTRL_OFF = 0; void FluidSequencer::init(const PlaybackSetupData& setupData, const std::optional& programOverride, - bool useDynamicEvents) + bool useDynamicEvents, const channel_t maxChannels) { TRACEFUNC; - m_channels.init(setupData, programOverride); + m_channels.init(setupData, programOverride, maxChannels); m_useDynamicEvents = useDynamicEvents; } -int FluidSequencer::currentExpressionLevel() const +bool FluidSequencer::usesDynamicEvents() const { - if (m_useDynamicEvents) { - return expressionLevel(dynamicLevel(m_playbackPosition)); + return m_useDynamicEvents; +} + +std::vector FluidSequencer::currentExpressionLevels() const +{ + std::vector result; + + if (!m_useDynamicEvents) { + return result; + } + + for (const auto& [layerIdx, curve] : m_playbackData.dynamics) { + if (curve.empty()) { + continue; + } + + auto voiceIt = m_channels.data().find(layerIdx); + if (voiceIt == m_channels.data().cend()) { + continue; + } + + const int level = expressionLevel(mpe::dynamicLevelFromNormalized(mpe::evaluateCurveAt(curve, m_playbackPosition))); + for (const auto& [_, mapping] : voiceIt->second) { + result.push_back({ mapping.first, level }); + } } - return naturalExpressionLevel(); + return result; } int FluidSequencer::naturalExpressionLevel() const @@ -59,18 +82,7 @@ int FluidSequencer::naturalExpressionLevel() const return NATURAL_EXP_LVL; } -void FluidSequencer::updateOffStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicLevelLayers& dynamics) -{ - addPlaybackEvents(m_offStreamEvents, events); - - if (m_useDynamicEvents) { - addDynamicEvents(m_offStreamEvents, dynamics); - } - - updateOffSequenceIterator(); -} - -void FluidSequencer::updateMainStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicLevelLayers& dynamics) +void FluidSequencer::updateMainStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicAutomationLayers& dynamics) { m_mainStreamEvents.clear(); @@ -87,6 +99,12 @@ void FluidSequencer::updateMainStreamEvents(const mpe::PlaybackEventsMap& events updateMainSequenceIterator(); } +void FluidSequencer::updateOffStreamEvents(const mpe::PlaybackEventsMap& events) +{ + addPlaybackEvents(m_offStreamEvents, events); + updateOffSequenceIterator(); +} + muse::async::Channel FluidSequencer::channelAdded() const { return m_channels.channelAdded; @@ -119,16 +137,33 @@ void FluidSequencer::addPlaybackEvents(EventSequenceMap& destination, const mpe: addSostenutoEvents(destination, sostenutoTimeAndDurations); } -void FluidSequencer::addDynamicEvents(EventSequenceMap& destination, const mpe::DynamicLevelLayers& dynamics) +void FluidSequencer::addDynamicEvents(EventSequenceMap& destination, const mpe::DynamicAutomationLayers& layers) { - for (const auto& layer : dynamics) { - for (const auto& dynamic : layer.second) { - midi::Event event(muse::midi::Event::Opcode::ControlChange, Event::MessageType::ChannelVoice10); - event.setIndex(midi::EXPRESSION_CONTROLLER); - event.setData(expressionLevel(dynamic.second)); + constexpr mpe::timestamp_t STEP_INTERVAL_US = 30000; - destination[dynamic.first].emplace_back(std::move(event)); + for (const auto& [layerIdx, curve] : layers) { + auto voiceIt = m_channels.data().find(layerIdx); + if (voiceIt == m_channels.data().cend()) { + continue; } + + std::optional lastLevel; + + mpe::resampleCurve(curve, STEP_INTERVAL_US, [&](mpe::timestamp_t t, muse::real_t normalized) { + const int level = expressionLevel(mpe::dynamicLevelFromNormalized(normalized)); + if (lastLevel == level) { + return; + } + lastLevel = level; + + for (const auto& [_, mapping] : voiceIt->second) { + midi::Event event(Event::Opcode::ControlChange, Event::MessageType::ChannelVoice10); + event.setChannel(mapping.first); + event.setIndex(midi::EXPRESSION_CONTROLLER); + event.setData(level); + destination[t].emplace_back(event); + } + }); } } @@ -136,7 +171,7 @@ void FluidSequencer::addNoteEvent(EventSequenceMap& destination, const mpe::Note SostenutoTimeAndDurations& sostenutoTimeAndDurations) { const ArrangementContext& arrangementCtx = noteEvent.arrangementCtx(); - const channel_t channelIdx = channel(noteEvent); + const channel_t channelIdx = resolveChannel(noteEvent); const note_idx_t noteIdx = noteIndex(noteEvent.pitchCtx().nominalPitchLevel); const velocity_t velocity = noteVelocity(noteEvent); const tuning_t tuning = noteTuning(noteEvent, noteIdx); @@ -146,6 +181,14 @@ void FluidSequencer::addNoteEvent(EventSequenceMap& destination, const mpe::Note m_lastStaff = noteEvent.arrangementCtx().staffLayerIndex; if (arrangementCtx.hasStart()) { + if (m_useDynamicEvents) { + midi::Event expressionEvent(Event::Opcode::ControlChange, Event::MessageType::ChannelVoice10); + expressionEvent.setChannel(channelIdx); + expressionEvent.setIndex(midi::EXPRESSION_CONTROLLER); + expressionEvent.setData(expressionLevel(noteEvent.expressionCtx().nominalDynamicLevel)); + destination[arrangementCtx.actualTimestamp].emplace_back(std::move(expressionEvent)); + } + midi::Event noteOn(Event::Opcode::NoteOn, Event::MessageType::ChannelVoice20); noteOn.setChannel(channelIdx); noteOn.setNote(noteIdx); @@ -332,7 +375,7 @@ void FluidSequencer::addSostenutoEvents(EventSequenceMap& destination, const Sos } } -channel_t FluidSequencer::channel(const mpe::NoteEvent& noteEvent) const +channel_t FluidSequencer::resolveChannel(const mpe::NoteEvent& noteEvent) const { return m_channels.resolveChannelForEvent(noteEvent); } diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.h b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.h index 78062eab52..ae9d6bc726 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.h +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.h @@ -34,9 +34,16 @@ namespace muse::audio::synth { class FluidSequencer : public engine::AbstractEventSequencer { public: - void init(const mpe::PlaybackSetupData& setupData, const std::optional& programOverride, bool useDynamicEvents); + void init(const mpe::PlaybackSetupData& setupData, const std::optional& programOverride, bool useDynamicEvents, + midi::channel_t maxChannels); - int currentExpressionLevel() const; + struct ChannelExpressionLevel { + midi::channel_t channel = 0; + int level = 0; + }; + + bool usesDynamicEvents() const; + std::vector currentExpressionLevels() const; int naturalExpressionLevel() const; async::Channel channelAdded() const; @@ -45,13 +52,13 @@ class FluidSequencer : public engine::AbstractEventSequencer int lastStaff() const; private: - void updateOffStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicLevelLayers& dynamics) override; - void updateMainStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicLevelLayers& dynamics) override; + void updateMainStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicAutomationLayers& dynamics) override; + void updateOffStreamEvents(const mpe::PlaybackEventsMap& events) override; using SostenutoTimeAndDurations = std::map >; void addPlaybackEvents(EventSequenceMap& destination, const mpe::PlaybackEventsMap& events); - void addDynamicEvents(EventSequenceMap& destination, const mpe::DynamicLevelLayers& dynamics); + void addDynamicEvents(EventSequenceMap& destination, const mpe::DynamicAutomationLayers& layers); void addNoteEvent(EventSequenceMap& destination, const mpe::NoteEvent& noteEvent, SostenutoTimeAndDurations& sostenutoTimeAndDurations); void addPedalEvent(EventSequenceMap& destination, const mpe::ArticulationMeta& meta, const midi::channel_t channelIdx); void addControlChangeEvent(EventSequenceMap& destination, const mpe::timestamp_t timestamp, const mpe::ControllerChangeEvent& event); @@ -63,7 +70,7 @@ class FluidSequencer : public engine::AbstractEventSequencer const uint32_t value); void addSostenutoEvents(EventSequenceMap& destination, const SostenutoTimeAndDurations& sostenutoTimeAndDurations); - midi::channel_t channel(const mpe::NoteEvent& noteEvent) const; + midi::channel_t resolveChannel(const mpe::NoteEvent& noteEvent) const; midi::note_idx_t noteIndex(const mpe::pitch_level_t pitchLevel) const; midi::tuning_t noteTuning(const mpe::NoteEvent& noteEvent, const int noteIdx) const; midi::velocity_t noteVelocity(const mpe::NoteEvent& noteEvent) const; diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.cpp b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.cpp index 88b8790f13..ddc5e3fe1e 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.cpp +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.cpp @@ -51,6 +51,8 @@ static constexpr unsigned int FLUID_AUDIO_CHANNELS_COUNT = FLUID_AUDIO_CHANNELS_ static constexpr bool STAFF_TO_MIDIOUT_CHANNEL = true; +static constexpr midi::channel_t MIDI_CHANNEL_COUNT = 16; + struct muse::audio::synth::Fluid { fluid_settings_t* settings = nullptr; fluid_synth_t* synth = nullptr; @@ -120,7 +122,7 @@ Ret FluidSynth::init(const OutputSpec& spec) fluid_settings_setint(m_fluid->settings, "synth.audio-channels", FLUID_AUDIO_CHANNELS_PAIR); // 1 pair of audio channels fluid_settings_setint(m_fluid->settings, "synth.lock-memory", 0); fluid_settings_setint(m_fluid->settings, "synth.threadsafe-api", 0); - fluid_settings_setint(m_fluid->settings, "synth.midi-channels", 16); + fluid_settings_setint(m_fluid->settings, "synth.midi-channels", MIDI_CHANNEL_COUNT); fluid_settings_setint(m_fluid->settings, "synth.dynamic-sample-loading", 1); fluid_settings_setint(m_fluid->settings, "synth.polyphony", 512); @@ -230,11 +232,7 @@ bool FluidSynth::handleEvent(const midi::Event& event) m_tuning.add(event.note(), event.pitchTuningCents()); } break; case Event::Opcode::ControlChange: { - if (event.index() == muse::midi::EXPRESSION_CONTROLLER) { - ret = setExpressionLevel(event.data7()); - } else { - ret = fluid_synth_cc(m_fluid->synth, event.channel(), event.index(), event.data7()); - } + ret = fluid_synth_cc(m_fluid->synth, event.channel(), event.index(), event.data7()); } break; case Event::Opcode::ProgramChange: { fluid_synth_program_change(m_fluid->synth, event.channel(), event.program()); @@ -274,7 +272,7 @@ void FluidSynth::setMode(const ProcessMode mode) AbstractSynthesizer::setMode(mode); m_sequencer.setActive(isModePlaying(mode)); - toggleExpressionController(); + updateExpressionLevels(); } void FluidSynth::setOutputSpec(const OutputSpec& spec) @@ -361,7 +359,7 @@ void FluidSynth::setupSound(const PlaybackSetupData& setupData) m_sequencer.channelAdded().onReceive(this, setupChannel, async::Asyncable::Mode::SetReplace); - m_sequencer.init(setupData, m_preset, setupData.supportsSingleNoteDynamics); + m_sequencer.init(setupData, m_preset, setupData.supportsSingleNoteDynamics, MIDI_CHANNEL_COUNT); for (const auto& voice : m_sequencer.channels().data()) { for (const auto& pair : voice.second) { @@ -404,7 +402,7 @@ void FluidSynth::setPlaybackPosition(const TimePosition& position) m_sequencer.setPlaybackPosition(msecs_t(usecs.raw())); if (m_sequencer.isActive()) { - setExpressionLevel(m_sequencer.currentExpressionLevel()); + updateExpressionLevels(); } } @@ -469,24 +467,26 @@ bool FluidSynth::processSequence(const FluidSequencer::EventSequence& sequence, return result == FLUID_OK; } -void FluidSynth::toggleExpressionController() +void FluidSynth::updateExpressionLevels() { + std::vector levels; if (m_sequencer.isActive()) { - setExpressionLevel(m_sequencer.currentExpressionLevel()); - } else { - setExpressionLevel(m_sequencer.naturalExpressionLevel()); + levels = m_sequencer.currentExpressionLevels(); } -} -int FluidSynth::setExpressionLevel(int level) -{ - midi::channel_t lastChannelIdx = m_sequencer.channels().lastIndex(); + if (!levels.empty()) { + for (const FluidSequencer::ChannelExpressionLevel& expressionLevel : levels) { + fluid_synth_cc(m_fluid->synth, expressionLevel.channel, muse::midi::EXPRESSION_CONTROLLER, expressionLevel.level); + } + return; + } + + const int naturalLevel = m_sequencer.naturalExpressionLevel(); + const midi::channel_t lastChannelIdx = m_sequencer.channels().lastIndex(); for (midi::channel_t i = 0; i < lastChannelIdx; ++i) { - fluid_synth_cc(m_fluid->synth, i, muse::midi::EXPRESSION_CONTROLLER, level); + fluid_synth_cc(m_fluid->synth, i, muse::midi::EXPRESSION_CONTROLLER, naturalLevel); } - - return FLUID_OK; } int FluidSynth::setControllerValue(int channel, int ctrl, int value) diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.h b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.h index 400f122c02..10fcf5c70e 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.h +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidsynth.h @@ -103,9 +103,8 @@ class FluidSynth : public AbstractSynthesizer bool processSequence(const FluidSequencer::EventSequence& sequence, const samples_t samples, float* buffer); bool handleEvent(const midi::Event& event); - void toggleExpressionController(); + void updateExpressionLevels(); - int setExpressionLevel(int level); int setControllerValue(int channel, int ctrl, int value); int setPitchBend(int channel, int pitchBend); diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h b/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h index 44902829a8..6c22295a7f 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h @@ -1106,9 +1106,13 @@ struct ChannelMap { using ChannelMapping = std::pair; using VoiceMappings = std::map; - void init(const mpe::PlaybackSetupData& setupData, const std::optional& programOverride) + void init(const mpe::PlaybackSetupData& setupData, const std::optional& programOverride, + const midi::channel_t maxChannels) { + m_maxChannels = maxChannels; m_programOverride = programOverride; + m_usesStaffLayers = setupData.supportsSingleNoteDynamics; + if (m_programOverride.has_value()) { resolveChannel(0, mpe::ArticulationType::Standard, m_programOverride.value()); } @@ -1127,8 +1131,12 @@ struct ChannelMap { midi::channel_t resolveChannelForEvent(const mpe::NoteEvent& event) { + //! NOTE: without single-note dynamics, voices don't need their own channel per staff + const mpe::staff_layer_idx_t staffIdx = m_usesStaffLayers ? event.arrangementCtx().staffLayerIndex : 0; + const mpe::layer_idx_t layerIdx = mpe::makeLayerIdx(staffIdx, event.arrangementCtx().voiceLayerIndex); + if (m_programOverride.has_value()) { - return resolveChannel(event.arrangementCtx().voiceLayerIndex, mpe::ArticulationType::Standard, m_programOverride.value()); + return resolveChannel(layerIdx, mpe::ArticulationType::Standard, m_programOverride.value()); } if (m_standardPrograms.empty()) { @@ -1137,13 +1145,14 @@ struct ChannelMap { const midi::Program& standardProgram = m_standardPrograms.at(0); - if (event.expressionCtx().articulations.contains(mpe::ArticulationType::Standard) - || event.expressionCtx().articulations.empty()) { - return resolveChannel(event.arrangementCtx().voiceLayerIndex, mpe::ArticulationType::Standard, standardProgram); - } + //! NOTE: always resolve the layer's own channel first: it must exist so that articulation + //! channels below have somewhere to fall back to once the channel budget runs out + const midi::channel_t standardChannel = resolveChannel(layerIdx, mpe::ArticulationType::Standard, standardProgram); - if (m_articulationMapping.empty()) { - return 0; + if (event.expressionCtx().articulations.contains(mpe::ArticulationType::Standard) + || event.expressionCtx().articulations.empty() + || m_articulationMapping.empty()) { + return standardChannel; } for (const auto& pair : event.expressionCtx().articulations) { @@ -1152,30 +1161,18 @@ struct ChannelMap { continue; } - return resolveChannel(event.arrangementCtx().voiceLayerIndex, search->first, search->second); + return resolveChannel(layerIdx, search->first, search->second, standardChannel, standardProgram); } - return resolveChannel(event.arrangementCtx().voiceLayerIndex, mpe::ArticulationType::Standard, standardProgram); + return standardChannel; } midi::channel_t lastIndex() const { - size_t result = 0; - - for (const auto& pair : m_data) { - result += pair.second.size(); - } - - return static_cast(result); + return m_channelCount; } - bool contains(const mpe::voice_layer_idx_t voiceIdx, const mpe::ArticulationType key) const - { - VoiceMappings& mapping = m_data[voiceIdx]; - return mapping.find(key) != mapping.cend(); - } - - const std::map& data() const + const std::map& data() const { return m_data; } @@ -1183,22 +1180,43 @@ struct ChannelMap { async::Channel channelAdded; private: - midi::channel_t resolveChannel(const mpe::voice_layer_idx_t voiceIdx, const mpe::ArticulationType type, const midi::Program& program) + //! Resolves the channel for a (layer, articulation) pair. Same-program articulations share a channel; + //! once m_maxChannels is exhausted, new ones fold onto the layer's own channel (fallbackChannel/fallbackProgram) + midi::channel_t resolveChannel(const mpe::layer_idx_t layerIdx, const mpe::ArticulationType type, const midi::Program& program, + const std::optional& fallbackChannel = std::nullopt, + const std::optional& fallbackProgram = std::nullopt) { - if (!contains(voiceIdx, type)) { - midi::channel_t newChannelIdx = lastIndex(); + VoiceMappings& mapping = m_data[layerIdx]; + + auto it = mapping.find(type); + if (it != mapping.cend()) { + return it->second.first; + } - m_data[voiceIdx].insert({ type, { newChannelIdx, program } }); - channelAdded.send(newChannelIdx, program); + for (const auto& entry : mapping) { + if (entry.second.second == program) { + mapping.insert({ type, entry.second }); + return entry.second.first; + } + } - return newChannelIdx; - } else { - return m_data[voiceIdx].at(type).first; + if (m_channelCount < m_maxChannels) { + const midi::channel_t channelIdx = m_channelCount++; + mapping.insert({ type, { channelIdx, program } }); + channelAdded.send(channelIdx, program); + return channelIdx; } + + const midi::channel_t channelIdx = fallbackChannel.value_or(0); + mapping.insert({ type, { channelIdx, fallbackProgram.value_or(program) } }); + return channelIdx; } - mutable std::map m_data; + std::map m_data; + midi::channel_t m_channelCount = 0; + midi::channel_t m_maxChannels = 16; + bool m_usesStaffLayers = false; std::optional m_programOverride; midi::Programs m_standardPrograms; ArticulationMapping m_articulationMapping; diff --git a/framework/audio/tests/rpcpacker_tests.cpp b/framework/audio/tests/rpcpacker_tests.cpp index 811b9612a5..2a4bb8a3ec 100644 --- a/framework/audio/tests/rpcpacker_tests.cpp +++ b/framework/audio/tests/rpcpacker_tests.cpp @@ -212,16 +212,16 @@ TEST_F(Audio_RpcPackerTests, ControlParams) TEST_F(Audio_RpcPackerTests, ControlParams_Automation) { - mpe::AutomationPoint::Bend bend; - bend.t = 0.3; - bend.value = 0.7; + mpe::AutomationPoint::Ease ease; + ease.t = 0.3; + ease.value = 0.7; mpe::AutomationPoint p0; p0.inValue = mpe::AutomationPoint::ArrivalFromPrevious {}; p0.outValue = 0.2; mpe::AutomationPoint p1; - p1.inValue = mpe::AutomationPoint::ExplicitArrival { real_t(0.4), bend }; + p1.inValue = mpe::AutomationPoint::ExplicitArrival { real_t(0.4), ease }; p1.outValue = 0.9; AutomationEnvelope envelope; diff --git a/framework/mpe/automationpoint.h b/framework/mpe/automationpoint.h index 2926bf1f02..daf120a13a 100644 --- a/framework/mpe/automationpoint.h +++ b/framework/mpe/automationpoint.h @@ -26,18 +26,22 @@ #include #include +#include "types/sharedmap.h" #include "types/number.h" +#include "containers.h" +#include "realfn.h" + namespace muse::mpe { struct AutomationPoint { - //! NOTE: bends the segment through point (t, value) - struct Bend { + //! NOTE: eases the segment through point (t, value) + struct Ease { real_t t = real_t::make(0.5); // [0; 1] real_t value = real_t::make(0.5); // [0; 1] - static constexpr Bend none() { return {}; } + static constexpr Ease none() { return {}; } bool isNone() const { return *this == none(); } - bool operator==(const Bend& b) const { return t == b.t && value == b.value; } + bool operator==(const Ease& e) const { return t == e.t && value == e.value; } }; //! NOTE: arrival value equals whatever precedes this point in the curve @@ -48,9 +52,9 @@ struct AutomationPoint { //! NOTE: arrival value is an explicit number struct ExplicitArrival { real_t value = 0.; - Bend bend; + Ease ease; - bool operator==(const ExplicitArrival& o) const { return value == o.value && bend == o.bend; } + bool operator==(const ExplicitArrival& o) const { return value == o.value && ease == o.ease; } }; using InValue = std::variant; @@ -64,15 +68,18 @@ struct AutomationPoint { } }; -//! NOTE: this point's bend, if it can have one; nullopt for ArrivalFromPrevious -inline std::optional bend(const AutomationPoint& point) noexcept +template +using AutomationCurve = SharedMap; + +//! NOTE: this point's ease, if it can have one; nullopt for ArrivalFromPrevious +inline std::optional ease(const AutomationPoint& point) noexcept { - return std::visit([](const auto& v) -> std::optional { + return std::visit([](const auto& v) -> std::optional { using T = std::decay_t; if constexpr (std::is_same_v) { return std::nullopt; } else { - return v.bend; + return v.ease; } }, point.inValue); } @@ -100,9 +107,9 @@ inline real_t evaluateAt(const AutomationPoint& point, std::optional pre const real_t prevOut = prevOutValue ? *prevOutValue : real_t(0.0); const real_t thisIn = resolveInValue(point, prevOutValue); const real_t range = thisIn - prevOut; - const std::optional pointBend = bend(point); + const std::optional pointEase = ease(point); - if (!pointBend || pointBend->isNone() || pointBend->t <= 0.0 || pointBend->t >= 1.0) { + if (!pointEase || pointEase->isNone() || pointEase->t <= 0.0 || pointEase->t >= 1.0) { return std::clamp(prevOut + range * t, real_t(0.0), real_t(1.0)); } @@ -111,21 +118,74 @@ inline real_t evaluateAt(const AutomationPoint& point, std::optional pre return std::clamp(u * u * p0 + 2.0 * u * s * p1 + s * s * p2, real_t(0.0), real_t(1.0)); }; - const real_t fraction = std::clamp(pointBend->value, real_t(0.0), real_t(1.0)); + const real_t fraction = std::clamp(pointEase->value, real_t(0.0), real_t(1.0)); const real_t bendValue = prevOut + fraction * range; const real_t lo = std::min(prevOut, thisIn); const real_t hi = std::max(prevOut, thisIn); const real_t halfSlope = 0.5 * range; - const real_t remainder = 1.0 - pointBend->t; + const real_t remainder = 1.0 - pointEase->t; // Clamped control points (q1, q2) for each arc - const real_t q1 = std::clamp(bendValue - pointBend->t * halfSlope, lo, hi); + const real_t q1 = std::clamp(bendValue - pointEase->t * halfSlope, lo, hi); - if (t <= pointBend->t) { - return quadraticBezier(t / pointBend->t, prevOut, q1, bendValue); + if (t <= pointEase->t) { + return quadraticBezier(t / pointEase->t, prevOut, q1, bendValue); } const real_t q2 = std::clamp(bendValue + remainder * halfSlope, lo, hi); - return quadraticBezier((t - pointBend->t) / remainder, bendValue, q2, thisIn); + return quadraticBezier((t - pointEase->t) / remainder, bendValue, q2, thisIn); +} + +template +inline real_t evaluateCurveAt(const AutomationCurve& curve, const typename AutomationCurve::key_type& pos) +{ + if (curve.empty()) { + return real_t {}; + } + + auto it = findLessOrEqual(curve, pos); + if (it == curve.end()) { + //! NOTE: hold the first point's value backwards in time, matching standard envelope semantics + return curve.begin()->second.outValue; + } + + real_t normalized = it->second.outValue; + const auto next = std::next(it); + if (next != curve.end()) { + const real_t t = static_cast(pos - it->first) / static_cast(next->first - it->first); + normalized = evaluateAt(next->second, it->second.outValue, t); + } + + return normalized; +} + +//! NOTE: resamples a curve into discrete samples for consumers that can't evaluate on read +template +void resampleCurve(const AutomationCurve& curve, Key stepInterval, Callback&& onSample) +{ + for (auto it = curve.cbegin(); it != curve.cend(); ++it) { + onSample(it->first, it->second.outValue); + + const auto next = std::next(it); + if (next == curve.cend()) { + continue; + } + + const real_t nextArrival = resolveInValue(next->second, it->second.outValue); + const std::optional nextEase = ease(next->second); + const bool isFlat = (!nextEase || nextEase->isNone()) && muse::RealIsEqual(nextArrival, it->second.outValue); + if (isFlat) { + continue; + } + + const Key intervalDuration = next->first - it->first; + const size_t steps = std::max(size_t((intervalDuration + stepInterval - 1) / stepInterval), size_t(1)); + + for (size_t j = 1; j < steps; ++j) { + const Key t = it->first + intervalDuration * j / steps; + const real_t normalized = evaluateAt(next->second, it->second.outValue, real_t(j) / real_t(steps)); + onSample(t, normalized); + } + } } } diff --git a/framework/mpe/events.h b/framework/mpe/events.h index ff2d925570..5ceb30e600 100644 --- a/framework/mpe/events.h +++ b/framework/mpe/events.h @@ -31,6 +31,7 @@ #include "types/number.h" #include "types/sharedmap.h" +#include "automationpoint.h" #include "mpetypes.h" #include "playbacksetupdata.h" @@ -341,16 +342,16 @@ using PlaybackEvent = std::variant; using PlaybackEventsMap = SharedMap; -using DynamicLevelMap = SharedMap; -using DynamicLevelLayers = SharedMap; +using DynamicAutomationMap = AutomationCurve; +using DynamicAutomationLayers = SharedMap; -using MainStreamChanges = async::Channel; -using OffStreamChanges = async::Channel; +using MainStreamChanges = async::Channel; +using OffStreamChanges = async::Channel; struct PlaybackData { PlaybackEventsMap originEvents; PlaybackSetupData setupData; - DynamicLevelLayers dynamics; + DynamicAutomationLayers dynamics; MainStreamChanges mainStream; OffStreamChanges offStream; diff --git a/framework/mpe/mpetypes.h b/framework/mpe/mpetypes.h index 91c33046dc..1da9d5ddec 100644 --- a/framework/mpe/mpetypes.h +++ b/framework/mpe/mpetypes.h @@ -32,6 +32,7 @@ #include "types/sharedhashmap.h" #include "types/sharedmap.h" +#include "types/number.h" #include "soundid.h" @@ -376,6 +377,11 @@ constexpr dynamic_level_t MAX_DYNAMIC_LEVEL = HUNDRED_PERCENT; constexpr dynamic_level_t MIN_DYNAMIC_LEVEL = 0; constexpr dynamic_level_t DYNAMIC_LEVEL_STEP = 5 * ONE_PERCENT; +inline dynamic_level_t dynamicLevelFromNormalized(const real_t normalized) +{ + return static_cast(lround(normalized.raw() * MAX_DYNAMIC_LEVEL)); +} + enum class DynamicType : percentage_t { Undefined = -1, ppppppppp = MIN_DYNAMIC_LEVEL, diff --git a/framework/musesampler/internal/musesamplersequencer.cpp b/framework/musesampler/internal/musesamplersequencer.cpp index 897f2a04fb..25d17f5e29 100644 --- a/framework/musesampler/internal/musesamplersequencer.cpp +++ b/framework/musesampler/internal/musesamplersequencer.cpp @@ -143,7 +143,21 @@ void MuseSamplerSequencer::init(MuseSamplerLibHandlerPtr samplerLib, ms_MuseSamp m_defaultPresetCode = std::move(defaultPresetCode); } -void MuseSamplerSequencer::updateOffStreamEvents(const PlaybackEventsMap& events, const DynamicLevelLayers&) +void MuseSamplerSequencer::updateMainStreamEvents(const PlaybackEventsMap& events, const DynamicAutomationLayers& dynamics) +{ + IF_ASSERT_FAILED(m_samplerLib && m_sampler) { + return; + } + + clearAllTracks(); + + loadEvents(events); + loadDynamicEvents(dynamics); + + finalizeAllTracks(); +} + +void MuseSamplerSequencer::updateOffStreamEvents(const PlaybackEventsMap& events) { m_auditionParamsCache.clear(); @@ -162,20 +176,6 @@ void MuseSamplerSequencer::updateOffStreamEvents(const PlaybackEventsMap& events updateOffSequenceIterator(); } -void MuseSamplerSequencer::updateMainStreamEvents(const PlaybackEventsMap& events, const DynamicLevelLayers& dynamics) -{ - IF_ASSERT_FAILED(m_samplerLib && m_sampler) { - return; - } - - clearAllTracks(); - - loadEvents(events); - loadDynamicEvents(dynamics); - - finalizeAllTracks(); -} - void MuseSamplerSequencer::clearAllTracks() { m_layerIdxToTrackIdx.clear(); @@ -280,17 +280,26 @@ void MuseSamplerSequencer::loadEvents(const PlaybackEventsMap& changes) } } -void MuseSamplerSequencer::loadDynamicEvents(const DynamicLevelLayers& changes) +void MuseSamplerSequencer::loadDynamicEvents(const DynamicAutomationLayers& changes) { + constexpr timestamp_t STEP_INTERVAL_US = 30000; + for (const auto& layer : changes) { ms_Track track = findTrack(layer.first); if (!track) { continue; } - for (const auto& dynamic : layer.second) { - m_samplerLib->addDynamicsEvent(m_sampler, track, DynamicEvent { dynamic.first, dynamicLevelRatio(dynamic.second) }); - } + std::optional lastRatio; + + resampleCurve(layer.second, STEP_INTERVAL_US, [&](timestamp_t t, real_t normalized) { + const double ratio = dynamicLevelRatio(dynamicLevelFromNormalized(normalized)); + if (lastRatio.has_value() && RealIsEqual(*lastRatio, ratio)) { + return; + } + lastRatio = ratio; + m_samplerLib->addDynamicsEvent(m_sampler, track, DynamicEvent { t, ratio }); + }); } } diff --git a/framework/musesampler/internal/musesamplersequencer.h b/framework/musesampler/internal/musesamplersequencer.h index 09b6a24c29..404dc23bf4 100644 --- a/framework/musesampler/internal/musesamplersequencer.h +++ b/framework/musesampler/internal/musesamplersequencer.h @@ -35,8 +35,8 @@ class MuseSamplerSequencer : public audio::engine::AbstractEventSequencer lastGain; + + mpe::resampleCurve(curve, STEP_INTERVAL_US, [&](mpe::timestamp_t t, muse::real_t normalized) { + const float gain = expressionLevel(mpe::dynamicLevelFromNormalized(normalized)); + if (lastGain.has_value() && muse::RealIsEqual(*lastGain, gain)) { + return; + } + lastGain = gain; + destination[t].emplace_back(gain); + }); } } @@ -131,6 +150,10 @@ void VstSequencer::addNoteEvent(EventSequenceMap& destination, const mpe::NoteEv const float tuning = noteTuning(noteEvent, noteId); if (arrangementCtx.hasStart()) { + if (m_useDynamicEvents) { + destination[arrangementCtx.actualTimestamp].emplace_back(expressionLevel(noteEvent.expressionCtx().nominalDynamicLevel)); + } + destination[arrangementCtx.actualTimestamp].emplace_back(buildEvent(VstEvent::kNoteOnEvent, noteId, velocityFraction, tuning)); } diff --git a/framework/vst/internal/synth/vstsequencer.h b/framework/vst/internal/synth/vstsequencer.h index 7b1a5b5350..8e979306ea 100644 --- a/framework/vst/internal/synth/vstsequencer.h +++ b/framework/vst/internal/synth/vstsequencer.h @@ -35,13 +35,13 @@ class VstSequencer : public audio::engine::AbstractEventSequencer; void addPlaybackEvents(EventSequenceMap& destination, const mpe::PlaybackEventsMap& events); - void addDynamicEvents(EventSequenceMap& destination, const mpe::DynamicLevelLayers& layers); + void addDynamicEvents(EventSequenceMap& destination, const mpe::DynamicAutomationLayers& layers); void addNoteEvent(EventSequenceMap& destination, const mpe::NoteEvent& noteEvent, SostenutoTimeAndDurations& sostenutoTimeAndDurations); void addPedalEvent(EventSequenceMap& destination, const mpe::ArticulationMeta& meta); void addControlChangeEvent(EventSequenceMap& destination, const mpe::timestamp_t timestamp, const mpe::ControllerChangeEvent& event);