Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion framework/audio/engine/internal/audiocontext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ RetVal2<TrackId, TrackParams> AudioContext::addTrack(const std::string& trackNam
};

// Make source
RetVal<AudioSourceNodePtr> source = audioFactory()->makeEventSource(trackId, playbackData, params.source, onOffStreamReceived);
RetVal<AudioSourceNodePtr> source = audioFactory()->makeEventSource(trackId, trackName, playbackData, params.source,
onOffStreamReceived);
if (!source.ret) {
return RetType::make_ret(source.ret);
}
Expand Down
5 changes: 3 additions & 2 deletions framework/audio/engine/internal/audiofactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ void AudioFactory::clearSynthSources()
synthResolver()->clearSources();
}

RetVal<AudioSourceNodePtr> AudioFactory::makeEventSource(const TrackId trackId, const mpe::PlaybackData& playbackData,
RetVal<AudioSourceNodePtr> AudioFactory::makeEventSource(const TrackId trackId, const std::string& trackName,
const mpe::PlaybackData& playbackData,
const AudioInputParams& params,
const std::function<void()> onOffStreamReceived) const
{
EventAudioNodePtr source = std::make_shared<EventAudioNode>(trackId, playbackData, onOffStreamReceived);
EventAudioNodePtr source = std::make_shared<EventAudioNode>(trackId, trackName, playbackData, onOffStreamReceived);
source->setOutputSpec(audioEngine()->outputSpec());
source->applyInputParams(params);
return RetVal<AudioSourceNodePtr>::make_ok(source);
Expand Down
3 changes: 2 additions & 1 deletion framework/audio/engine/internal/audiofactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ class AudioFactory : public IAudioFactory
RetVal<synth::ISynthesizerPtr> makeDefaultSynth(const TrackId trackId) const override;
void clearSynthSources() override;

RetVal<AudioSourceNodePtr> makeEventSource(const TrackId trackId, const mpe::PlaybackData& playbackData, const AudioInputParams& params,
RetVal<AudioSourceNodePtr> makeEventSource(const TrackId trackId, const std::string& trackName, const mpe::PlaybackData& playbackData,
const AudioInputParams& params,
const std::function<void()> onOffStreamReceived = nullptr) const override;

// Make FX
Expand Down
4 changes: 2 additions & 2 deletions framework/audio/engine/internal/iaudiofactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ class IAudioFactory : MODULE_GLOBAL_INTERFACE
// This method clears this registry.
virtual void clearSynthSources() = 0;

virtual RetVal<AudioSourceNodePtr> makeEventSource(const TrackId trackId, const mpe::PlaybackData& playbackData,
const AudioInputParams& params,
virtual RetVal<AudioSourceNodePtr> makeEventSource(const TrackId trackId, const std::string& trackName,
const mpe::PlaybackData& playbackData, const AudioInputParams& params,
const std::function<void()> onOffStreamReceived = nullptr) const = 0;
// Make FX
virtual FxChainPtr makeMasterFxChain(const AudioFxChain& fxChain) const = 0;
Expand Down
5 changes: 3 additions & 2 deletions framework/audio/engine/internal/nodes/eventaudionode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ using namespace muse::audio::engine;
using namespace muse::audio::synth;
using namespace muse::mpe;

EventAudioNode::EventAudioNode(TrackId trackId, const mpe::PlaybackData& playbackData,
EventAudioNode::EventAudioNode(TrackId trackId, const std::string& hostTrackName, const mpe::PlaybackData& playbackData,
OnOffStreamEventsReceived onOffStreamReceived)
: m_trackId(trackId), m_playbackData(playbackData)
: m_trackId(trackId), m_hostTrackName(hostTrackName), m_playbackData(playbackData)
{
ONLY_AUDIO_ENGINE_THREAD;

Expand Down Expand Up @@ -145,6 +145,7 @@ void EventAudioNode::applyInputParams(const AudioInputParams& requiredParams)
}

m_synth = synth.val;
m_synth->setHostTrackName(m_hostTrackName);

m_synth->paramsChanged().onReceive(this, [this](const AudioInputParams& params) {
m_paramsChanges.send(params);
Expand Down
4 changes: 3 additions & 1 deletion framework/audio/engine/internal/nodes/eventaudionode.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class EventAudioNode : public AudioSourceNode, public async::Asyncable
public:
using OnOffStreamEventsReceived = std::function<void ()>;

explicit EventAudioNode(TrackId trackId, const mpe::PlaybackData& playbackData, OnOffStreamEventsReceived onOffStreamReceived);
explicit EventAudioNode(TrackId trackId, const std::string& hostTrackName, const mpe::PlaybackData& playbackData,
OnOffStreamEventsReceived onOffStreamReceived);

~EventAudioNode() override;

Expand Down Expand Up @@ -78,6 +79,7 @@ class EventAudioNode : public AudioSourceNode, public async::Asyncable
void restoreSynthCtx(const SynthCtx& ctx);

TrackId m_trackId = -1;
std::string m_hostTrackName;
mpe::PlaybackData m_playbackData;
synth::ISynthesizerPtr m_synth = nullptr;
AudioInputParams m_params;
Expand Down
4 changes: 4 additions & 0 deletions framework/audio/engine/isynthesizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class ISynthesizer
virtual void setup(const mpe::PlaybackData& playbackData) = 0;
virtual const mpe::PlaybackData& playbackData() const = 0;

// Optional host track/instrument name, forwarded to instruments that auto-select their sound
// from it (e.g. via VST3 channel context). Ignored by default.
virtual void setHostTrackName(const std::string& /*name*/) {}

virtual const audio::AudioInputParams& params() const = 0;
virtual async::Channel<audio::AudioInputParams> paramsChanged() const = 0;

Expand Down
2 changes: 1 addition & 1 deletion framework/mpe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ target_sources(muse_mpe PRIVATE
playbacksetupdata.h
iarticulationprofilesrepository.h

internal/articulationstringutils.h
articulationstringutils.h
internal/articulationprofilesrepository.cpp
internal/articulationprofilesrepository.h
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
#include "mpe/mpetypes.h"

namespace muse::mpe {
static const std::unordered_map<ArticulationFamily, QString> ARTICULATION_FAMILY_NAMES {
inline const std::unordered_map<ArticulationFamily, QString> ARTICULATION_FAMILY_NAMES {
{ ArticulationFamily::Undefined, "Undefined" },
{ ArticulationFamily::Keyboards, "KeyboardsArticulation" },
{ ArticulationFamily::Strings, "StringsArticulation" },
Expand All @@ -37,7 +37,7 @@ static const std::unordered_map<ArticulationFamily, QString> ARTICULATION_FAMILY
{ ArticulationFamily::Voices, "VoicesArticulation" },
};

static const std::unordered_map<ArticulationType, QString> ARTICULATION_TYPE_NAMES {
inline const std::unordered_map<ArticulationType, QString> ARTICULATION_TYPE_NAMES {
{ ArticulationType::Undefined, "Undefined" },
{ ArticulationType::Standard, "Standard" },
{ ArticulationType::Staccato, "Staccato" },
Expand Down
2 changes: 1 addition & 1 deletion framework/mpe/internal/articulationprofilesrepository.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

#include "log.h"

#include "internal/articulationstringutils.h"
#include "articulationstringutils.h"

using namespace muse;
using namespace muse::mpe;
Expand Down
2 changes: 1 addition & 1 deletion framework/mpe/qml/Muse/Mpe/articulationpatternitem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

#include <iterator>

#include "internal/articulationstringutils.h"
#include "articulationstringutils.h"

using namespace muse::mpe;

Expand Down
4 changes: 4 additions & 0 deletions framework/vst/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ target_no_warning(muse_vst -Wno-deprecated-declarations)
if (MUSE_MODULE_VST_QML)
add_subdirectory(qml/Muse/Vst)
endif()

if (MUSE_MODULE_VST_TESTS)
add_subdirectory(tests)
endif()
166 changes: 153 additions & 13 deletions framework/vst/internal/synth/vstsequencer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,31 @@ static const mpe::ArticulationTypeSet BEND_SUPPORTED_TYPES {
mpe::ArticulationType::Multibend, mpe::ArticulationType::ContinuousGlissando,
};

void VstSequencer::init(ParamsMapping&& mapping, bool useDynamicEvents)
// "Span" articulations expand into many rapid sub-notes sharing one meta.timestamp; an instrument
// may render them as a single sustained gesture. A new span re-sends the same keyswitch so the
// instrument can retrigger. Other articulations are one note per keyswitch and never retrigger.
static const mpe::ArticulationTypeSet SPAN_ARTICULATION_TYPES {
mpe::ArticulationType::Tremolo8th, mpe::ArticulationType::Tremolo16th,
mpe::ArticulationType::Tremolo32nd, mpe::ArticulationType::Tremolo64th,
};

static bool isSpanArticulation(mpe::ArticulationType type)
{
return muse::contains(SPAN_ARTICULATION_TYPES, type);
}

// Keyswitch note the profile assigns to this articulation, or nullopt if it maps none.
static std::optional<int> keyswitchFor(const VstKeyswitchProfile& profile, mpe::ArticulationType type)
{
auto it = profile.keyswitches.find(type);
return it != profile.keyswitches.cend() ? std::optional<int>(it->second) : std::nullopt;
}

void VstSequencer::init(ParamsMapping&& mapping, bool useDynamicEvents, std::optional<VstKeyswitchProfile> keyswitchProfile)
{
m_mapping = std::move(mapping);
m_useDynamicEvents = useDynamicEvents;
m_keyswitchProfile = std::move(keyswitchProfile);
m_inited = true;

updateMainStreamEvents(m_playbackData.originEvents, m_playbackData.dynamics);
Expand Down Expand Up @@ -80,6 +101,7 @@ void VstSequencer::updateMainStreamEvents(const mpe::PlaybackEventsMap& events,
void VstSequencer::updateOffStreamEvents(const mpe::PlaybackEventsMap& events)
{
addPlaybackEvents(m_offStreamEvents, events);
sortNoteOnEventsByPitch(m_offStreamEvents);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
updateOffSequenceIterator();
}

Expand Down Expand Up @@ -111,11 +133,12 @@ muse::audio::gain_t VstSequencer::currentGain() const
void VstSequencer::addPlaybackEvents(EventSequenceMap& destination, const mpe::PlaybackEventsMap& events)
{
SostenutoTimeAndDurations sostenutoTimeAndDurations;
LastKeyswitchPerTimestamp lastKeyswitch;

for (const auto& evPair : events) {
for (const mpe::PlaybackEvent& event : evPair.second) {
if (std::holds_alternative<mpe::NoteEvent>(event)) {
addNoteEvent(destination, std::get<mpe::NoteEvent>(event), sostenutoTimeAndDurations);
addNoteEvent(destination, std::get<mpe::NoteEvent>(event), sostenutoTimeAndDurations, lastKeyswitch);
} else if (std::holds_alternative<mpe::ControllerChangeEvent>(event)) {
addControlChangeEvent(destination, evPair.first, std::get<mpe::ControllerChangeEvent>(event));
}
Expand Down Expand Up @@ -160,7 +183,8 @@ void VstSequencer::addDynamicEvents(EventSequenceMap& destination, const mpe::Dy
}

void VstSequencer::addNoteEvent(EventSequenceMap& destination, const mpe::NoteEvent& noteEvent,
SostenutoTimeAndDurations& sostenutoTimeAndDurations)
SostenutoTimeAndDurations& sostenutoTimeAndDurations,
LastKeyswitchPerTimestamp& lastKeyswitch)
{
const mpe::ArrangementContext& arrangementCtx = noteEvent.arrangementCtx();
const int32_t noteId = noteIndex(noteEvent.pitchCtx().nominalPitchLevel);
Expand All @@ -180,6 +204,57 @@ void VstSequencer::addNoteEvent(EventSequenceMap& destination, const mpe::NoteEv
destination[timestampTo].emplace_back(buildEvent(VstEvent::kNoteOffEvent, noteId, velocityFraction, tuning));
}

// Latching keyswitch with span retrigger: send a keyswitch NoteOn when the articulation changes,
// or when a span articulation starts a new span. The keyswitch persists until a different one is
// sent (no NoteOff); re-sending the same one signals a span retrigger.
if (m_keyswitchProfile.has_value() && arrangementCtx.hasStart()) {
const VstKeyswitchProfile& profile = *m_keyswitchProfile;
// Default to the Standard keyswitch, or to nothing when the instrument does not advertise it:
// never fabricate a pitch the instrument did not advertise.
std::optional<int> keyswitchPitch = keyswitchFor(profile, mpe::ArticulationType::Standard);
mpe::timestamp_t spanStart = -1;

// Pick by precedence: a specific timbre (pizzicato, mute, harmonic, staccato...) beats a
// tremolo, which beats normal. On a tie between two primary timbres on one note, take the
// lowest keyswitch note, so the choice is deterministic and not the hash map's iteration order.
int bestRank = -1;
for (const auto& artPair : noteEvent.expressionCtx().articulations) {
if (mpe::isRangedArticulation(artPair.first)) {
continue; // forwarded as its own span below, never the primary keyswitch
}
const std::optional<int> mapped = keyswitchFor(profile, artPair.first);
if (!mapped.has_value()) {
continue;
}
const bool span = isSpanArticulation(artPair.first);
const bool standard = (artPair.first == mpe::ArticulationType::Standard);
const int rank = standard ? 0 : (span ? 1 : 2);
if (rank > bestRank || (rank == bestRank && mapped.value() < keyswitchPitch.value())) {
bestRank = rank;
keyswitchPitch = mapped;
spanStart = span ? artPair.second.meta.timestamp : -1;
}
}

// upper_bound, not lower_bound: std::prev then yields the most recent state at or before this
// timestamp even when an earlier note of the same chord already inserted an entry at it, so a
// chord sharing one articulation does not re-emit a duplicate keyswitch per note.
auto it = lastKeyswitch.upper_bound(arrangementCtx.actualTimestamp);
LastKeyswitchState prevState;
if (it != lastKeyswitch.begin()) {
prevState = std::prev(it)->second;
}

if (keyswitchPitch.has_value()
&& (keyswitchPitch.value() != prevState.keyswitch || (spanStart != -1 && spanStart != prevState.spanStart))) {
// Full velocity: a keyswitch is a control signal, not a note. A soft dynamic rounds to
// velocity 0, which a receiver reads as a note-off and drops, losing the switch.
destination[arrangementCtx.actualTimestamp].emplace_back(
buildEvent(VstEvent::kNoteOnEvent, keyswitchPitch.value(), 1.f, 0.f));
lastKeyswitch[arrangementCtx.actualTimestamp] = { keyswitchPitch.value(), spanStart };
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
for (const auto& artPair : noteEvent.expressionCtx().articulations) {
if (artPair.first == mpe::ArticulationType::Standard) {
continue;
Expand All @@ -202,6 +277,58 @@ void VstSequencer::addNoteEvent(EventSequenceMap& destination, const mpe::NoteEv
sostenutoTimeAndDurations.push_back(mpe::TimestampAndDuration { timestamp, meta.overallDuration });
continue;
}

// A ranged articulation the instrument advertises as a keyswitch is forwarded as a keyswitch
// spanning the range: pressed at the start, released at the end. The instrument decides what
// the modifier means; the sequencer only reports when it is active.
if (m_keyswitchProfile.has_value() && mpe::isRangedArticulation(meta.type)) {
if (const std::optional<int> pitch = keyswitchFor(*m_keyswitchProfile, meta.type)) {
addKeyswitchSpanEvent(destination, meta, arrangementCtx.actualTimestamp, *pitch);
}
continue;
}
}
}

void VstSequencer::addKeyswitchSpanEvent(EventSequenceMap& destination, const mpe::ArticulationMeta& meta,
const mpe::timestamp_t noteTimestamp, int keyswitchPitch)
{
// Press at every covered note's onset, not once at the range start, so starting playback partway
// through the range still engages it. Dedup within one onset (a chord shares it), like the
// release below, so we do not queue N identical note-ons at one instant. Full velocity (a
// keyswitch is a control signal, see above).
EventSequence& onset = destination[noteTimestamp];
bool alreadyPressed = false;
for (const EventType& queued : onset) {
if (std::holds_alternative<VstEvent>(queued)) {
const VstEvent& ev = std::get<VstEvent>(queued);
if (ev.type == VstEvent::kNoteOnEvent && ev.noteOn.pitch == keyswitchPitch) {
alreadyPressed = true;
break;
}
}
}
if (!alreadyPressed) {
onset.emplace_back(buildEvent(VstEvent::kNoteOnEvent, keyswitchPitch, 1.f, 0.f));
}

if (meta.hasEnd()) {
// Dedup the release: every covered note computes the same range-end, so without this we queue
// N identical note-offs at one instant and can overflow the host block's fixed-size event list.
EventSequence& bucket = destination[meta.timestamp + meta.overallDuration];
bool alreadyReleased = false;
for (const EventType& queued : bucket) {
if (std::holds_alternative<VstEvent>(queued)) {
const VstEvent& ev = std::get<VstEvent>(queued);
if (ev.type == VstEvent::kNoteOffEvent && ev.noteOff.pitch == keyswitchPitch) {
alreadyReleased = true;
break;
}
}
}
if (!alreadyReleased) {
bucket.emplace_back(buildEvent(VstEvent::kNoteOffEvent, keyswitchPitch, 1.f, 0.f));
}
}
}

Expand Down Expand Up @@ -342,20 +469,33 @@ void VstSequencer::sortNoteOnEventsByPitch(EventSequenceMap& destination)
continue;
}

std::stable_sort(seq.begin(), seq.end(), [](const EventType& e1, const EventType& e2) {
if (!std::holds_alternative<VstEvent>(e1) || !std::holds_alternative<VstEvent>(e2)) {
return false;
// Reorder only the NoteOn events (by pitch), leaving every other event (NoteOff, param and
// gain changes) exactly where it is. Sorting the whole sequence with a comparator that
// returned "equal" for non-NoteOn events would not be a strict weak ordering.
std::vector<VstEvent> noteOns;
for (const EventType& event : seq) {
if (std::holds_alternative<VstEvent>(event)) {
const VstEvent& vstEvent = std::get<VstEvent>(event);
if (vstEvent.type == VstEvent::kNoteOnEvent) {
noteOns.push_back(vstEvent);
}
}
}

const VstEvent& ve1 = std::get<VstEvent>(e1);
const VstEvent& ve2 = std::get<VstEvent>(e2);

if (ve1.type == VstEvent::kNoteOnEvent && ve2.type == VstEvent::kNoteOnEvent) {
return ve1.noteOn.pitch < ve2.noteOn.pitch;
}
if (noteOns.size() <= 1) {
continue;
}

return false;
std::stable_sort(noteOns.begin(), noteOns.end(), [](const VstEvent& e1, const VstEvent& e2) {
return e1.noteOn.pitch < e2.noteOn.pitch;
});

size_t noteOnIdx = 0;
for (EventType& event : seq) {
if (std::holds_alternative<VstEvent>(event) && std::get<VstEvent>(event).type == VstEvent::kNoteOnEvent) {
event = noteOns[noteOnIdx++];
}
}
}
}

Expand Down
Loading