Skip to content
Draft
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
145 changes: 136 additions & 9 deletions driver/fluidsyX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,26 @@ struct FluidsyXApp {
};
std::vector<PendingSngNoteEvent> pendingSngEvents;

#if FLUID_VERSION_AT_LEAST(2, 6, 0)
/** Per-voice callback data, keyed by user-controlled voice ID (ctx.voiceId).
* Entries are inserted in dummy_preset_noteon() when a voice is started,
* and removed in voiceStateCallback() when the voice finishes. */
struct VoiceCallbackData {
FluidsyXApp* app;
int macroId;
unsigned int voiceId; /**< Key in voiceCallbacks — needed for self-removal on FINISHED */
};
std::map<unsigned int, VoiceCallbackData> voiceCallbacks;

/** Invoked from the FluidSynth synthesis thread when a voice enters its
* release phase (NOTEOFF) or finishes playing (FINISHED).
* Sets the appropriate received flag and, for indefinite waits only,
* schedules an immediate timer to resume the blocked macro. */
static void voiceStateCallback(fluid_voice_t* voice,
enum fluid_voice_callback_reason reason,
void* data);
#endif

/* ── lifecycle helpers ── */

[[nodiscard]] bool initFluidSynth();
Expand Down Expand Up @@ -1016,6 +1036,11 @@ struct FluidsyXApp {
void killVoice(fluid_synth_t* synth, MacroExecContext& ctx);
/** Kick off the next pending timer step for all active macros */
void scheduleNextTimerStep();

/** Helper: schedule an immediate sequencer timer to resume macro \p macroId.
* Called from voiceStateCallback (synthesis thread) and the NoteOff handler
* (sequencer thread) when an indefinite wait condition is satisfied. */
void scheduleImmediateResume(int macroId);
};

/* ═══════════════════ Dummy preset callbacks ═══════════════════ */
Expand Down Expand Up @@ -1143,6 +1168,34 @@ static int dummy_preset_noteon(fluid_preset_t* preset, fluid_synth_t* synth,

app->applyAdsrToVoice(voice, ctx, false);

#if FLUID_VERSION_AT_LEAST(2, 6, 0)
/* Register a voice-state callback so the macro can react to the true
* noteoff (voice enters release) and voice finish (sample end) events.
*
* Registration is done here, BEFORE fluid_synth_start_voice(), because
* this is the only synchronous opportunity: dummy_preset_noteon is called
* from within fluid_synth_start() before the voice becomes active in the
* DSP loop. Voice callbacks can only fire from the audio/synthesis thread
* during fluid_synth_process(), which cannot run until after
* fluid_synth_start_voice() returns. There is therefore no risk of a
* premature callback.
*
* std::map (tree-based) guarantees that inserting new entries never
* invalidates references or pointers to existing entries, so &cbData
* remains valid for the lifetime of the entry. */
{
int macroId = ctx.selfId;
unsigned int vid = static_cast<unsigned int>(fluid_voice_get_id(voice));
if (macroId >= 0 && vid != 0) {
auto& cbData = app->voiceCallbacks[vid];
cbData.app = app;
cbData.macroId = macroId;
cbData.voiceId = vid;
fluid_voice_set_callback(voice, FluidsyXApp::voiceStateCallback, &cbData);
}
}
#endif

fluid_synth_start_voice(synth, voice);
return FLUID_OK;
}
Expand Down Expand Up @@ -1175,9 +1228,11 @@ bool FluidsyXApp::initFluidSynth() {
fluid_settings_setint(settings.get(), "audio.period-size", 256);
fluid_settings_setint(settings.get(), "synth.verbose", 0);
fluid_settings_setnum(settings.get(), "synth.gain", 0.9);
fluid_settings_setnum(settings.get(), "synth.reverb.active", 1);
fluid_settings_setstr(settings.get(), "synth.reverb.engine", "lex");
fluid_settings_setnum(settings.get(), "synth.reverb.level", 0.8);
fluid_settings_setnum(settings.get(), "synth.reverb.room-size", 0.7);
fluid_settings_setnum(settings.get(), "synth.reverb.width", 1);
fluid_settings_setnum(settings.get(), "synth.reverb.width", 2);
fluid_settings_setnum(settings.get(), "synth.reverb.damp", 0);
// Use FluidSynth's linear portamento mode via the portamento-time setting.
fluid_settings_setstr(settings.get(), "synth.portamento-time", "linear");
Expand Down Expand Up @@ -1770,12 +1825,12 @@ unsigned int SoundMacro::CmdWaitTicks::DoFluid(MacroExecContext& ctx, fluid_voic
if(ticksOrMs != 0)
{
// Step 1 – Keyoff pre-check (synthmacros.c:76-86): If the Keyoff flag is set and if a keyoff has already been received and if the sustain pedal is not held: return 0
if(v)
{
if (ctx.waitingKeyoff && ctx.keyoffReceived && !fluid_voice_is_sustained(v) && !fluid_voice_is_sostenuto(v)) {
ctx.waitingKeyoff = false;
return 0;
}
// Note: when v==nullptr (voice already released), treat as "not sustained".
if (ctx.waitingKeyoff && ctx.keyoffReceived &&
(!v || (!fluid_voice_is_sustained(v) && !fluid_voice_is_sostenuto(v)))) {
ctx.waitingKeyoff = false;
ctx.pc++;
return 0;
}
// If the Keyoff flag is not set: the "wake on keyoff" flag is cleared (cFlags &= ~4).
ctx.waitingKeyoff = keyOff;
Expand All @@ -1784,6 +1839,7 @@ unsigned int SoundMacro::CmdWaitTicks::DoFluid(MacroExecContext& ctx, fluid_voic
if (ctx.waitingSampleEnd && ctx.sampleEndReceived) {
ctx.waitingKeyoff = false;
ctx.waitingSampleEnd = false;
ctx.pc++;
return 0;
}
// If the Sampleend flag is not set: the "wake on sampleend" flag is cleared (cFlags &= ~0x40000).
Expand Down Expand Up @@ -2893,6 +2949,11 @@ int FluidsyXApp::enqueueSoundMacro(const SoundMacro* sm, int step,
ctx.appData = this;
ctx.macStartTime = startTick;

/* Pre-assign the macroId so that dummy_preset_noteon() can embed it in
* the voice callback before the context is inserted into activeMacros. */
int macroId = nextMacroId++;
ctx.selfId = macroId;

/* Walk through commands that execute instantly (no delay).
* When a command introduces a delay, schedule a timer event and stop. */
unsigned int tick = startTick;
Expand All @@ -2902,15 +2963,13 @@ int FluidsyXApp::enqueueSoundMacro(const SoundMacro* sm, int step,
if (ctx.inIndefiniteWait) {
/* Indefinite wait – store context but do NOT schedule a timer.
* The macro will be resumed by an external event (keyoff, sampleEnd). */
int macroId = nextMacroId++;
activeMacros[macroId] = ctx;
return macroId;
}
tick += d;
safetyCounter++;
if (d > 0 && !ctx.ended) {
/* Store context in the stable map and schedule a timer callback */
int macroId = nextMacroId++;
activeMacros[macroId] = ctx;

FluidEventPtr tevt(new_fluid_event(), &delete_fluid_event);
Expand All @@ -2921,6 +2980,11 @@ int FluidsyXApp::enqueueSoundMacro(const SoundMacro* sm, int step,
return macroId;
}
}
#if FLUID_VERSION_AT_LEAST(2, 6, 0)
/* Macro completed synchronously without waiting — however the voice (if
* any) may still be playing. The voiceCallbacks entry remains valid;
* the FINISHED callback will erase it when the voice actually stops. */
#endif
return -1;
}

Expand Down Expand Up @@ -3068,6 +3132,69 @@ void FluidsyXApp::timerCallback(unsigned int time, fluid_event_t* event,
app->activeMacros.erase(it);
}

void FluidsyXApp::scheduleImmediateResume(int macroId) {
FluidEventPtr resumeEvt(new_fluid_event(), &delete_fluid_event);
fluid_event_set_source(resumeEvt.get(), callbackSeqId);
fluid_event_set_dest(resumeEvt.get(), callbackSeqId);
fluid_event_timer(resumeEvt.get(), reinterpret_cast<void*>(static_cast<intptr_t>(macroId)));
fluid_sequencer_send_now(sequencer.get(), resumeEvt.get());
}

#if FLUID_VERSION_AT_LEAST(2, 6, 0)
void FluidsyXApp::voiceStateCallback(fluid_voice_t* /*voice*/,
int reason,
void* data) {
auto* cbData = static_cast<VoiceCallbackData*>(data);
if (!cbData)
return;
FluidsyXApp* app = cbData->app;
int macroId = cbData->macroId;
if (!app)
return;

auto it = app->activeMacros.find(macroId);

switch (reason) {
case FLUID_VOICE_CALLBACK_NOTEOFF:
/* The voice has entered its true release phase (not sustained/sostenutoed).
* Treat this the same as receiving a MIDI NoteOff for the macro. */
if (it != app->activeMacros.end()) {
MacroExecContext& ctx = it->second;
if (!ctx.keyoffReceived) {
ctx.keyoffReceived = true;
/* Only schedule an immediate resume for indefinite waits; for
* timed waits the existing timer will fire and check the flag. */
if (ctx.inIndefiniteWait && ctx.waitingKeyoff) {
ctx.inIndefiniteWait = false;
ctx.waitingKeyoff = false;
app->scheduleImmediateResume(macroId);
}
}
}
break;

case FLUID_VOICE_CALLBACK_FINISHED:
/* The voice has finished playing and is being removed from the DSP loop.
* This is the closest proxy for "sample end" that FluidSynth exposes. */
if (it != app->activeMacros.end()) {
MacroExecContext& ctx = it->second;
if (!ctx.sampleEndReceived) {
ctx.sampleEndReceived = true;
if (ctx.inIndefiniteWait && ctx.waitingSampleEnd) {
ctx.inIndefiniteWait = false;
ctx.waitingSampleEnd = false;
app->scheduleImmediateResume(macroId);
}
}
}
/* Erase the per-voice callback data regardless of whether the macro
* is still alive – the voice no longer exists after this callback. */
app->voiceCallbacks.erase(cbData->voiceId);
break;
}
}
#endif

/* ═══════════════════ SNG → FluidSynth sequencer scheduling ═══════════════════ */

double FluidsyXApp::scheduleSongEvents(const uint8_t* sngData, size_t /*sngSize*/) {
Expand Down
5 changes: 5 additions & 0 deletions include/amuse/FluidsyXMacroContext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ struct MacroExecContext {
unsigned int voiceId = 0;
uint8_t triggerNote = 60; /**< Original SNG note (before keymap/layer transpose), used for NoteOff matching */

/** Key of this context in FluidsyXApp::activeMacros. Pre-assigned in
* enqueueSoundMacro() so that dummy_preset_noteon() can register a voice
* callback carrying the macroId before the context is inserted into the map. */
int selfId = -1;

/* ── Pending SoundMacro commands ──
* Commands executed before CmdStartSample creates the voice are stored
* here. We cannot allocate the voice earlier (e.g. in FluidsyXApp::enqueueSoundMacro()), because fluidsynth wants to know
Expand Down