From 61b421c3c2165bc91a11ab614212f4fc8fdfde1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:33:07 +0000 Subject: [PATCH 01/14] Add fluid_voice_set_callback() API for voice state change notifications Implement a new public API function fluid_voice_set_callback() that allows applications to register a callback to be notified when: - A true noteoff is processed (FLUID_VOICE_CALLBACK_NOTEOFF) - The voice finishes playing and is removed from DSP (FLUID_VOICE_CALLBACK_FINISHED) Changes: - Add fluid_voice_callback_reason enum and fluid_voice_callback_t typedef to include/fluidsynth/voice.h - Add fluid_voice_set_callback() declaration to public header - Add callback/callback_data fields to struct _fluid_voice_t - Implement callback invocation in fluid_voice_release() and fluid_voice_stop() - Clear callback fields on voice re-initialization in fluid_voice_init() - Add unit test test_voice_callback.c Agent-Logs-Url: https://github.com/derselbst/fluidsynth/sessions/3c1d7aa5-1096-44d4-ae88-631b25ff90c3 Co-authored-by: derselbst <8152480+derselbst@users.noreply.github.com> --- include/fluidsynth/voice.h | 32 +++++++ src/synth/fluid_voice.c | 50 +++++++++++ src/synth/fluid_voice.h | 4 + test/CMakeLists.txt | 1 + test/test_voice_callback.c | 177 +++++++++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 test/test_voice_callback.c diff --git a/include/fluidsynth/voice.h b/include/fluidsynth/voice.h index 86010401d..daa36c7fe 100644 --- a/include/fluidsynth/voice.h +++ b/include/fluidsynth/voice.h @@ -50,6 +50,37 @@ enum fluid_voice_add_mod FLUID_VOICE_DEFAULT /**< For default modulators only, no need to check for duplicates */ }; +/** + * Enum indicating the reason a voice callback was invoked. + * + * @since 2.5.4 + */ +enum fluid_voice_callback_reason +{ + FLUID_VOICE_CALLBACK_NOTEOFF, /**< A true noteoff was processed for this voice, i.e. the voice + is neither sustained nor sostenutoed and has entered + its release phase. */ + FLUID_VOICE_CALLBACK_FINISHED /**< The voice has finished playing and is about to be + removed from the DSP loop. */ +}; + +/** + * Callback function type for voice events. + * + * @param voice The voice instance that triggered the callback. + * @param reason The reason why the callback was invoked (see #fluid_voice_callback_reason). + * @param data User-defined data pointer as passed to fluid_voice_set_callback(). + * + * @note This callback is invoked from the synthesis thread context. + * The callback implementation must not call any FluidSynth API function + * that could trigger voice or synth modifications, as this may lead to + * deadlocks or data corruption. The callback should be kept as short as + * possible. + * + * @since 2.5.4 + */ +typedef void (*fluid_voice_callback_t)(fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data); + FLUIDSYNTH_API void fluid_voice_add_mod(fluid_voice_t *voice, fluid_mod_t *mod, int mode); FLUIDSYNTH_API float fluid_voice_gen_get(fluid_voice_t *voice, int gen); FLUIDSYNTH_API void fluid_voice_gen_set(fluid_voice_t *voice, int gen, float val); @@ -67,6 +98,7 @@ FLUIDSYNTH_API int fluid_voice_is_sustained(const fluid_voice_t *voice); FLUIDSYNTH_API int fluid_voice_is_sostenuto(const fluid_voice_t *voice); FLUIDSYNTH_API int fluid_voice_optimize_sample(fluid_sample_t *s); FLUIDSYNTH_API void fluid_voice_update_param(fluid_voice_t *voice, int gen); +FLUIDSYNTH_API void fluid_voice_set_callback(fluid_voice_t *voice, fluid_voice_callback_t callback, void *data); /** @} */ #ifdef __cplusplus diff --git a/src/synth/fluid_voice.c b/src/synth/fluid_voice.c index 126b583a3..5270c2798 100644 --- a/src/synth/fluid_voice.c +++ b/src/synth/fluid_voice.c @@ -322,6 +322,8 @@ fluid_voice_init(fluid_voice_t *voice, fluid_sample_t *sample, voice->mod_count = 0; voice->start_time = start_time; voice->has_noteoff = 0; + voice->callback = NULL; + voice->callback_data = NULL; UPDATE_RVOICE0(fluid_rvoice_reset); /* @@ -1339,6 +1341,11 @@ fluid_voice_release(fluid_voice_t *voice) unsigned int at_tick = fluid_channel_get_min_note_length_ticks(voice->channel); UPDATE_RVOICE_I1(fluid_rvoice_noteoff, at_tick); voice->has_noteoff = 1; // voice is marked as noteoff occurred + + if(voice->callback != NULL) + { + voice->callback(voice, FLUID_VOICE_CALLBACK_NOTEOFF, voice->callback_data); + } } /* @@ -1457,6 +1464,11 @@ fluid_voice_stop(fluid_voice_t *voice) { fluid_profile(FLUID_PROF_VOICE_RELEASE, voice->ref, 0, 0); + if(voice->callback != NULL) + { + voice->callback(voice, FLUID_VOICE_CALLBACK_FINISHED, voice->callback_data); + } + voice->chan = NO_CHANNEL; /* Decrement the reference count of the sample, to indicate @@ -1644,6 +1656,44 @@ int fluid_voice_is_sostenuto(const fluid_voice_t *voice) return (voice->status == FLUID_VOICE_HELD_BY_SOSTENUTO); } +/** + * Set a callback function for a voice to be notified about voice state changes. + * + * The callback is invoked when: + * - A true noteoff is processed for this voice (i.e. the voice enters the release + * phase and is neither sustained nor sostenutoed), with reason + * #FLUID_VOICE_CALLBACK_NOTEOFF. + * - The voice has finished playing and is about to be removed from the DSP loop, + * with reason #FLUID_VOICE_CALLBACK_FINISHED. + * + * Only one callback function can be registered per voice. Setting a new callback + * replaces the previous one. Passing NULL as the callback removes any previously + * registered callback. + * + * The callback is automatically cleared when the voice is re-initialized for a + * new note. + * + * @param voice Voice instance + * @param callback Callback function to register, or NULL to unregister. + * @param data User-defined data pointer passed to the callback. + * + * @note This function should be called after fluid_synth_alloc_voice() and before + * fluid_synth_start_voice(). + * + * @warning The callback is invoked from the synthesis thread context. + * The callback implementation must not call any FluidSynth API function + * that could trigger voice or synth modifications, as this may lead to + * deadlocks or data corruption. + * + * @since 2.5.4 + */ +void fluid_voice_set_callback(fluid_voice_t *voice, fluid_voice_callback_t callback, void *data) +{ + fluid_return_if_fail(voice != NULL); + voice->callback = callback; + voice->callback_data = data; +} + /** * Return the MIDI channel the voice is playing on. * diff --git a/src/synth/fluid_voice.h b/src/synth/fluid_voice.h index 542ffeb12..f4d3f4aa8 100644 --- a/src/synth/fluid_voice.h +++ b/src/synth/fluid_voice.h @@ -112,6 +112,10 @@ struct _fluid_voice_t char can_access_overflow_rvoice; /* False if overflow_rvoice is being rendered in separate thread */ char has_noteoff; /* Flag set when noteoff has been sent */ + /* user callback */ + fluid_voice_callback_t callback; + void *callback_data; + #ifdef WITH_PROFILING /* for debugging */ double ref; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0ee168b3f..4a61cd111 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -39,6 +39,7 @@ ADD_FLUID_TEST(test_synth_render_s32) ADD_FLUID_TEST(test_round_clip) ADD_FLUID_TEST(test_ABI) ADD_FLUID_TEST(test_file_seek_tell) +ADD_FLUID_TEST(test_voice_callback) if ( GLIB_SUPPORT AND GLib2_VERSION VERSION_GREATER_EQUAL 2.33.12 ) # Earlier versions of GLib had broken comment handling and should not be compared to diff --git a/test/test_voice_callback.c b/test/test_voice_callback.c new file mode 100644 index 000000000..27db87285 --- /dev/null +++ b/test/test_voice_callback.c @@ -0,0 +1,177 @@ + +#include "test.h" +#include "fluidsynth.h" + +/* C90: all variable declarations at top of blocks */ + +static int noteoff_count = 0; +static int finished_count = 0; +static void *noteoff_data_received = NULL; +static void *finished_data_received = NULL; +static unsigned int noteoff_voice_id = 0; +static unsigned int finished_voice_id = 0; + +static void voice_callback(fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data) +{ + if(reason == FLUID_VOICE_CALLBACK_NOTEOFF) + { + noteoff_count++; + noteoff_data_received = data; + noteoff_voice_id = fluid_voice_get_id(voice); + } + else if(reason == FLUID_VOICE_CALLBACK_FINISHED) + { + finished_count++; + finished_data_received = data; + finished_voice_id = fluid_voice_get_id(voice); + } +} + +/* Render some audio to advance the synth state */ +static void render_frames(fluid_synth_t *synth, int frames) +{ + int i; + float left[1024], right[1024]; + float *bufs[2]; + bufs[0] = left; + bufs[1] = right; + + for(i = 0; i < frames; i += 1024) + { + int count = frames - i; + if(count > 1024) + { + count = 1024; + } + fluid_synth_process(synth, count, 2, bufs, 0, NULL); + } +} + +int main(void) +{ + fluid_settings_t *settings; + fluid_synth_t *synth; + int sfont_id; + int marker = 42; + + settings = new_fluid_settings(); + TEST_ASSERT(settings != NULL); + + /* Disable reverb and chorus for faster/cleaner test */ + TEST_SUCCESS(fluid_settings_setint(settings, "synth.reverb.active", 0)); + TEST_SUCCESS(fluid_settings_setint(settings, "synth.chorus.active", 0)); + + synth = new_fluid_synth(settings); + TEST_ASSERT(synth != NULL); + + sfont_id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 1); + TEST_ASSERT(sfont_id != FLUID_FAILED); + + /* === Test 1: Callback receives noteoff and finished events === */ + noteoff_count = 0; + finished_count = 0; + noteoff_data_received = NULL; + finished_data_received = NULL; + + /* Play a note using the public MIDI API (noteon) */ + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 100)); + + /* Render a small amount to let the voice start */ + render_frames(synth, 512); + + /* Now set the callback on all playing voices. + * Since we used noteon, we look for voices on channel 0, key 60. */ + { + int i; + fluid_voice_t *voices[256]; + int voice_count = 0; + + /* Use fluid_synth_get_voicelist to find the active voice */ + fluid_synth_get_voicelist(synth, voices, 256, -1); + + for(i = 0; i < 256; i++) + { + if(voices[i] == NULL) + { + break; + } + if(fluid_voice_get_key(voices[i]) == 60 && + fluid_voice_get_channel(voices[i]) == 0 && + fluid_voice_is_playing(voices[i])) + { + fluid_voice_set_callback(voices[i], voice_callback, &marker); + voice_count++; + } + } + TEST_ASSERT(voice_count > 0); + } + + /* Send noteoff */ + TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60)); + + /* Render some frames to process the noteoff */ + render_frames(synth, 512); + + /* The noteoff callback should have been invoked at least once */ + TEST_ASSERT(noteoff_count > 0); + TEST_ASSERT(noteoff_data_received == &marker); + + /* Render a large number of frames to let the voice finish its release phase. + * Use all_sounds_off to force immediate release if needed. */ + render_frames(synth, 44100 * 2); + fluid_synth_all_sounds_off(synth, 0); + render_frames(synth, 44100 * 2); + + /* The finished callback should have been invoked */ + TEST_ASSERT(finished_count > 0); + TEST_ASSERT(finished_data_received == &marker); + + /* === Test 2: Removing callback with NULL === */ + noteoff_count = 0; + finished_count = 0; + + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 64, 100)); + render_frames(synth, 512); + + { + fluid_voice_t *voices[256]; + int i; + int voice_count = 0; + + fluid_synth_get_voicelist(synth, voices, 256, -1); + + for(i = 0; i < 256; i++) + { + if(voices[i] == NULL) + { + break; + } + if(fluid_voice_get_key(voices[i]) == 64 && + fluid_voice_get_channel(voices[i]) == 0 && + fluid_voice_is_playing(voices[i])) + { + /* Set callback then remove it */ + fluid_voice_set_callback(voices[i], voice_callback, &marker); + fluid_voice_set_callback(voices[i], NULL, NULL); + voice_count++; + } + } + TEST_ASSERT(voice_count > 0); + } + + TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 64)); + render_frames(synth, 512); + + /* Callback was removed, so counts should remain 0 */ + TEST_ASSERT(noteoff_count == 0); + + fluid_synth_all_sounds_off(synth, 0); + render_frames(synth, 44100 * 2); + TEST_ASSERT(finished_count == 0); + + /* cleanup */ + delete_fluid_synth(synth); + delete_fluid_settings(settings); + + return EXIT_SUCCESS; +} From d642e602eb38f597f01e9ca8b94a488633fe7eec Mon Sep 17 00:00:00 2001 From: derselbst Date: Mon, 13 Apr 2026 21:26:23 +0200 Subject: [PATCH 02/14] Fix unit test to provide the expected behavior --- test/test_voice_callback.c | 121 +++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 33 deletions(-) diff --git a/test/test_voice_callback.c b/test/test_voice_callback.c index 27db87285..0bad55346 100644 --- a/test/test_voice_callback.c +++ b/test/test_voice_callback.c @@ -1,8 +1,9 @@ #include "test.h" #include "fluidsynth.h" +#include "fluid_midi.h" -/* C90: all variable declarations at top of blocks */ +enum { Polyphony = 64 }; static int noteoff_count = 0; static int finished_count = 0; @@ -30,21 +31,7 @@ static void voice_callback(fluid_voice_t *voice, enum fluid_voice_callback_reaso /* Render some audio to advance the synth state */ static void render_frames(fluid_synth_t *synth, int frames) { - int i; - float left[1024], right[1024]; - float *bufs[2]; - bufs[0] = left; - bufs[1] = right; - - for(i = 0; i < frames; i += 1024) - { - int count = frames - i; - if(count > 1024) - { - count = 1024; - } - fluid_synth_process(synth, count, 2, bufs, 0, NULL); - } + fluid_synth_process(synth, frames, 0, NULL, 0, NULL); } int main(void) @@ -61,6 +48,8 @@ int main(void) TEST_SUCCESS(fluid_settings_setint(settings, "synth.reverb.active", 0)); TEST_SUCCESS(fluid_settings_setint(settings, "synth.chorus.active", 0)); + TEST_SUCCESS(fluid_settings_setint(settings, "synth.polyphony", Polyphony)); + synth = new_fluid_synth(settings); TEST_ASSERT(synth != NULL); @@ -77,19 +66,19 @@ int main(void) TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 100)); /* Render a small amount to let the voice start */ - render_frames(synth, 512); + render_frames(synth, fluid_synth_get_internal_bufsize(synth)); /* Now set the callback on all playing voices. * Since we used noteon, we look for voices on channel 0, key 60. */ { int i; - fluid_voice_t *voices[256]; + fluid_voice_t *voices[Polyphony]; int voice_count = 0; /* Use fluid_synth_get_voicelist to find the active voice */ - fluid_synth_get_voicelist(synth, voices, 256, -1); + fluid_synth_get_voicelist(synth, voices, Polyphony, -1); - for(i = 0; i < 256; i++) + for(i = 0; i < Polyphony; i++) { if(voices[i] == NULL) { @@ -100,6 +89,9 @@ int main(void) fluid_voice_is_playing(voices[i])) { fluid_voice_set_callback(voices[i], voice_callback, &marker); + // Set the voice to instant release + fluid_voice_gen_set(voices[i], GEN_VOLENVRELEASE, -32768); + fluid_voice_update_param(voices[i], GEN_VOLENVRELEASE); voice_count++; } } @@ -109,9 +101,6 @@ int main(void) /* Send noteoff */ TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60)); - /* Render some frames to process the noteoff */ - render_frames(synth, 512); - /* The noteoff callback should have been invoked at least once */ TEST_ASSERT(noteoff_count > 0); TEST_ASSERT(noteoff_data_received == &marker); @@ -119,8 +108,6 @@ int main(void) /* Render a large number of frames to let the voice finish its release phase. * Use all_sounds_off to force immediate release if needed. */ render_frames(synth, 44100 * 2); - fluid_synth_all_sounds_off(synth, 0); - render_frames(synth, 44100 * 2); /* The finished callback should have been invoked */ TEST_ASSERT(finished_count > 0); @@ -131,16 +118,15 @@ int main(void) finished_count = 0; TEST_SUCCESS(fluid_synth_noteon(synth, 0, 64, 100)); - render_frames(synth, 512); + render_frames(synth, fluid_synth_get_internal_bufsize(synth)); { - fluid_voice_t *voices[256]; + fluid_voice_t *voices[Polyphony]; int i; int voice_count = 0; - fluid_synth_get_voicelist(synth, voices, 256, -1); - - for(i = 0; i < 256; i++) + fluid_synth_get_voicelist(synth, voices, Polyphony, -1); + for(i = 0; i < Polyphony; i++) { if(voices[i] == NULL) { @@ -153,6 +139,9 @@ int main(void) /* Set callback then remove it */ fluid_voice_set_callback(voices[i], voice_callback, &marker); fluid_voice_set_callback(voices[i], NULL, NULL); + // Set the voice to instant release + fluid_voice_gen_set(voices[i], GEN_VOLENVRELEASE, -32768); + fluid_voice_update_param(voices[i], GEN_VOLENVRELEASE); voice_count++; } } @@ -160,14 +149,80 @@ int main(void) } TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 64)); - render_frames(synth, 512); + render_frames(synth, fluid_synth_get_internal_bufsize(synth)); + TEST_SUCCESS(fluid_synth_all_sounds_off(synth, 0)); + render_frames(synth, 44100 * 2); /* Callback was removed, so counts should remain 0 */ TEST_ASSERT(noteoff_count == 0); + TEST_ASSERT(finished_count == 0); + + /* === Test 3: Same as test 1, but activate sustain to defer noteOff callback === */ + noteoff_count = 0; + finished_count = 0; + noteoff_data_received = NULL; + finished_data_received = NULL; + + /* Play a note using the public MIDI API (noteon) */ + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 100)); + + /* Render a small amount to let the voice start */ + render_frames(synth, fluid_synth_get_internal_bufsize(synth)); + + { + int i; + fluid_voice_t *voices[Polyphony]; + int voice_count = 0; + + /* Use fluid_synth_get_voicelist to find the active voice */ + fluid_synth_get_voicelist(synth, voices, Polyphony, -1); + + for(i = 0; i < Polyphony; i++) + { + if(voices[i] == NULL) + { + break; + } + if(fluid_voice_get_key(voices[i]) == 60 && + fluid_voice_get_channel(voices[i]) == 0 && + fluid_voice_is_playing(voices[i])) + { + fluid_voice_set_callback(voices[i], voice_callback, &marker); + // Set the voice to instant release + fluid_voice_gen_set(voices[i], GEN_VOLENVRELEASE, -32768); + fluid_voice_update_param(voices[i], GEN_VOLENVRELEASE); + voice_count++; + } + } + TEST_ASSERT(voice_count > 0); + } + + fluid_synth_cc(synth, 0, SUSTAIN_SWITCH, 127); + + /* Send noteoff */ + TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60)); + + // no callback yet because sustain is on + TEST_ASSERT(noteoff_count == 0); + + // render some frames + render_frames(synth, fluid_synth_get_internal_bufsize(synth)); + + // still no callback + TEST_ASSERT(noteoff_count == 0); + TEST_ASSERT(noteoff_data_received == NULL); + + fluid_synth_cc(synth, 0, SUSTAIN_SWITCH, 0); + TEST_ASSERT(noteoff_count > 0); + TEST_ASSERT(noteoff_data_received == &marker); - fluid_synth_all_sounds_off(synth, 0); + /* Render a large number of frames to let the voice finish its release phase. + * Use all_sounds_off to force immediate release if needed. */ render_frames(synth, 44100 * 2); - TEST_ASSERT(finished_count == 0); + + /* The finished callback should have been invoked */ + TEST_ASSERT(finished_count > 0); + TEST_ASSERT(finished_data_received == &marker); /* cleanup */ delete_fluid_synth(synth); From 8050a29aabe7f5663e36e833448abb938ec12c30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:37:26 +0000 Subject: [PATCH 03/14] Fire FINISHED callback immediately after rendering, not deferred to next API call Move finished voice processing into fluid_synth_render_blocks() right after fluid_rvoice_mixer_render() completes. This ensures the FINISHED callback fires as soon as the voice finishes rendering, regardless of whether any subsequent synth API call is made. Agent-Logs-Url: https://github.com/derselbst/fluidsynth/sessions/735fc152-b315-4555-a553-672d551927b3 Co-authored-by: derselbst <8152480+derselbst@users.noreply.github.com> --- src/synth/fluid_synth.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/synth/fluid_synth.c b/src/synth/fluid_synth.c index 83974f91a..c9e1acd82 100644 --- a/src/synth/fluid_synth.c +++ b/src/synth/fluid_synth.c @@ -5093,6 +5093,12 @@ fluid_synth_render_blocks(fluid_synth_t *synth, int blockcount) blockcount = fluid_rvoice_mixer_render(synth->eventhandler->mixer, blockcount); + /* Process voices that have finished rendering, so that their + * callbacks (if any) fire immediately rather than being deferred + * until the next API call enters fluid_synth_api_enter(). + */ + fluid_synth_check_finished_voices(synth); + /* Testcase, that provokes a denormal floating point error */ #if 0 { From f3f1b41a020e24628efd08b83a79f74919010031 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:49:53 +0000 Subject: [PATCH 04/14] Fix thread safety: fire FINISHED callback from rvoice layer in render thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach called fluid_synth_check_finished_voices() from fluid_synth_render_blocks(), which created two consumers on the SPSC finished_voices ringbuffer — a data race between the render thread and the API thread (in fluid_synth_api_enter). New approach: - Add finished callback fields to fluid_rvoice_t (function ptr, voice ptr, data) - Add fluid_rvoice_set_finished_callback() event function dispatched via the event queue from fluid_voice_set_callback() - Fire the FINISHED callback directly from the mixer's fluid_mixer_buffer_process_finished_voices() in the render thread, before pushing to the ringbuffer - Remove FINISHED callback from fluid_voice_stop() (now redundant) This maintains the SPSC invariant: the render thread is both the producer of finished voice notifications AND the invoker of the callback, while the API thread remains the sole consumer of the ringbuffer. Agent-Logs-Url: https://github.com/derselbst/fluidsynth/sessions/735fc152-b315-4555-a553-672d551927b3 Co-authored-by: derselbst <8152480+derselbst@users.noreply.github.com> --- src/rvoice/fluid_rvoice.c | 15 +++++++++++++++ src/rvoice/fluid_rvoice.h | 15 +++++++++++++++ src/rvoice/fluid_rvoice_mixer.c | 7 +++++++ src/synth/fluid_synth.c | 6 ------ src/synth/fluid_voice.c | 33 ++++++++++++++++++++++++++++----- 5 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/rvoice/fluid_rvoice.c b/src/rvoice/fluid_rvoice.c index 2b52b78c4..dd168e903 100644 --- a/src/rvoice/fluid_rvoice.c +++ b/src/rvoice/fluid_rvoice.c @@ -573,6 +573,11 @@ DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_reset) fluid_iir_filter_reset(&voice->resonant_filter); fluid_iir_filter_reset(&voice->resonant_custom_filter); + /* Clear finished callback */ + voice->finished_cb = NULL; + voice->finished_cb_voice = NULL; + voice->finished_cb_data = NULL; + /* Force setting of the phase at the first DSP loop run * This cannot be done earlier, because it depends on modulators. [DH] Is that comment really true? */ @@ -946,3 +951,13 @@ DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_voiceoff) } +DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_set_finished_callback) +{ + fluid_rvoice_t *voice = obj; + + voice->finished_cb = (fluid_rvoice_finished_cb_t) param[0].ptr; + voice->finished_cb_voice = param[1].ptr; + voice->finished_cb_data = param[2].ptr; +} + + diff --git a/src/rvoice/fluid_rvoice.h b/src/rvoice/fluid_rvoice.h index 2bb77f43b..c68a4bf6a 100644 --- a/src/rvoice/fluid_rvoice.h +++ b/src/rvoice/fluid_rvoice.h @@ -154,6 +154,14 @@ struct _fluid_rvoice_buffers_t }; +/** + * Generic callback for rvoice finished notification. + * Invoked from the render thread when the rvoice completes. + * @param voice Opaque pointer (typically the fluid_voice_t*) + * @param data User-defined data pointer + */ +typedef void (*fluid_rvoice_finished_cb_t)(void *voice, void *data); + /* * Hard realtime parameters needed to synthesize a voice */ @@ -164,6 +172,12 @@ struct _fluid_rvoice_t fluid_iir_filter_t resonant_filter; /* IIR resonant dsp filter */ fluid_iir_filter_t resonant_custom_filter; /* optional custom/general-purpose IIR resonant filter */ fluid_rvoice_buffers_t buffers; + + /* Finished callback, invoked from the render thread when the rvoice + * finishes and is about to be removed from the mixer's active list. */ + fluid_rvoice_finished_cb_t finished_cb; + void *finished_cb_voice; /* fluid_voice_t* passed as first arg */ + void *finished_cb_data; /* user data passed as second arg */ }; @@ -196,6 +210,7 @@ DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_set_loopstart); DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_set_loopend); DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_set_samplemode); DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_set_sample); +DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_set_finished_callback); int fluid_rvoice_dsp_silence(fluid_rvoice_t *rvoice, fluid_real_t *FLUID_RESTRICT dsp_buf, int looping); diff --git a/src/rvoice/fluid_rvoice_mixer.c b/src/rvoice/fluid_rvoice_mixer.c index 985383d68..813b1a4aa 100644 --- a/src/rvoice/fluid_rvoice_mixer.c +++ b/src/rvoice/fluid_rvoice_mixer.c @@ -391,6 +391,13 @@ fluid_mixer_buffer_process_finished_voices(fluid_mixer_buffers_t *buffers) buffers->mixer->active_voices = av; + /* Fire user-registered finished callback from the render thread, + * before the voice is pushed to the finished_voices ringbuffer. */ + if(v->finished_cb != NULL) + { + v->finished_cb(v->finished_cb_voice, v->finished_cb_data); + } + fluid_rvoice_eventhandler_finished_voice_callback(buffers->mixer->eventhandler, v); } diff --git a/src/synth/fluid_synth.c b/src/synth/fluid_synth.c index c9e1acd82..83974f91a 100644 --- a/src/synth/fluid_synth.c +++ b/src/synth/fluid_synth.c @@ -5093,12 +5093,6 @@ fluid_synth_render_blocks(fluid_synth_t *synth, int blockcount) blockcount = fluid_rvoice_mixer_render(synth->eventhandler->mixer, blockcount); - /* Process voices that have finished rendering, so that their - * callbacks (if any) fire immediately rather than being deferred - * until the next API call enters fluid_synth_api_enter(). - */ - fluid_synth_check_finished_voices(synth); - /* Testcase, that provokes a denormal floating point error */ #if 0 { diff --git a/src/synth/fluid_voice.c b/src/synth/fluid_voice.c index 5270c2798..387c12e71 100644 --- a/src/synth/fluid_voice.c +++ b/src/synth/fluid_voice.c @@ -1464,11 +1464,6 @@ fluid_voice_stop(fluid_voice_t *voice) { fluid_profile(FLUID_PROF_VOICE_RELEASE, voice->ref, 0, 0); - if(voice->callback != NULL) - { - voice->callback(voice, FLUID_VOICE_CALLBACK_FINISHED, voice->callback_data); - } - voice->chan = NO_CHANNEL; /* Decrement the reference count of the sample, to indicate @@ -1656,6 +1651,21 @@ int fluid_voice_is_sostenuto(const fluid_voice_t *voice) return (voice->status == FLUID_VOICE_HELD_BY_SOSTENUTO); } +/* + * Trampoline for the rvoice finished callback. + * Called from the render thread when the rvoice finishes. + * Invokes the user's callback with FLUID_VOICE_CALLBACK_FINISHED. + */ +static void fluid_voice_finished_cb_trampoline(void *voice_ptr, void *data) +{ + fluid_voice_t *voice = (fluid_voice_t *)voice_ptr; + + if(voice->callback != NULL) + { + voice->callback(voice, FLUID_VOICE_CALLBACK_FINISHED, data); + } +} + /** * Set a callback function for a voice to be notified about voice state changes. * @@ -1689,9 +1699,22 @@ int fluid_voice_is_sostenuto(const fluid_voice_t *voice) */ void fluid_voice_set_callback(fluid_voice_t *voice, fluid_voice_callback_t callback, void *data) { + fluid_rvoice_param_t param[MAX_EVENT_PARAMS]; + fluid_return_if_fail(voice != NULL); + voice->callback = callback; voice->callback_data = data; + + /* Propagate to the rvoice so the finished callback fires from the + * render thread immediately when the voice finishes, rather than + * being deferred to the next API call. */ + param[0].ptr = (callback != NULL) ? (void *)fluid_voice_finished_cb_trampoline : NULL; + param[1].ptr = voice; + param[2].ptr = data; + fluid_rvoice_eventhandler_push(voice->eventhandler, + fluid_rvoice_set_finished_callback, + voice->rvoice, param); } /** From dcdd04c24df784038622808e43392c6b2125b416 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:54:42 +0000 Subject: [PATCH 05/14] Remove trampoline: store user callback directly on rvoice for thread safety Eliminate the intermediate trampoline that read voice->callback from the API-thread-owned fluid_voice_t. Instead, store the user's callback function pointer directly on the rvoice (ABI-compatible: void* vs fluid_voice_t*, int vs enum). This ensures the render thread only reads data from the rvoice (set via the event queue), never from the API-thread's fluid_voice_t. Agent-Logs-Url: https://github.com/derselbst/fluidsynth/sessions/735fc152-b315-4555-a553-672d551927b3 Co-authored-by: derselbst <8152480+derselbst@users.noreply.github.com> --- src/rvoice/fluid_rvoice.h | 3 ++- src/rvoice/fluid_rvoice_mixer.c | 2 +- src/synth/fluid_voice.c | 21 ++++----------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/rvoice/fluid_rvoice.h b/src/rvoice/fluid_rvoice.h index c68a4bf6a..5bfd7b431 100644 --- a/src/rvoice/fluid_rvoice.h +++ b/src/rvoice/fluid_rvoice.h @@ -158,9 +158,10 @@ struct _fluid_rvoice_buffers_t * Generic callback for rvoice finished notification. * Invoked from the render thread when the rvoice completes. * @param voice Opaque pointer (typically the fluid_voice_t*) + * @param reason Integer reason code (FLUID_VOICE_CALLBACK_FINISHED) * @param data User-defined data pointer */ -typedef void (*fluid_rvoice_finished_cb_t)(void *voice, void *data); +typedef void (*fluid_rvoice_finished_cb_t)(void *voice, int reason, void *data); /* * Hard realtime parameters needed to synthesize a voice diff --git a/src/rvoice/fluid_rvoice_mixer.c b/src/rvoice/fluid_rvoice_mixer.c index 813b1a4aa..ab022c67c 100644 --- a/src/rvoice/fluid_rvoice_mixer.c +++ b/src/rvoice/fluid_rvoice_mixer.c @@ -395,7 +395,7 @@ fluid_mixer_buffer_process_finished_voices(fluid_mixer_buffers_t *buffers) * before the voice is pushed to the finished_voices ringbuffer. */ if(v->finished_cb != NULL) { - v->finished_cb(v->finished_cb_voice, v->finished_cb_data); + v->finished_cb(v->finished_cb_voice, 1 /* FLUID_VOICE_CALLBACK_FINISHED */, v->finished_cb_data); } fluid_rvoice_eventhandler_finished_voice_callback(buffers->mixer->eventhandler, v); diff --git a/src/synth/fluid_voice.c b/src/synth/fluid_voice.c index 387c12e71..04614f012 100644 --- a/src/synth/fluid_voice.c +++ b/src/synth/fluid_voice.c @@ -1651,21 +1651,6 @@ int fluid_voice_is_sostenuto(const fluid_voice_t *voice) return (voice->status == FLUID_VOICE_HELD_BY_SOSTENUTO); } -/* - * Trampoline for the rvoice finished callback. - * Called from the render thread when the rvoice finishes. - * Invokes the user's callback with FLUID_VOICE_CALLBACK_FINISHED. - */ -static void fluid_voice_finished_cb_trampoline(void *voice_ptr, void *data) -{ - fluid_voice_t *voice = (fluid_voice_t *)voice_ptr; - - if(voice->callback != NULL) - { - voice->callback(voice, FLUID_VOICE_CALLBACK_FINISHED, data); - } -} - /** * Set a callback function for a voice to be notified about voice state changes. * @@ -1708,8 +1693,10 @@ void fluid_voice_set_callback(fluid_voice_t *voice, fluid_voice_callback_t callb /* Propagate to the rvoice so the finished callback fires from the * render thread immediately when the voice finishes, rather than - * being deferred to the next API call. */ - param[0].ptr = (callback != NULL) ? (void *)fluid_voice_finished_cb_trampoline : NULL; + * being deferred to the next API call. + * The user's fluid_voice_callback_t is ABI-compatible with + * fluid_rvoice_finished_cb_t (void*, int, void*). */ + param[0].ptr = (void *)callback; param[1].ptr = voice; param[2].ptr = data; fluid_rvoice_eventhandler_push(voice->eventhandler, From 330d6af1a5f60899e552c9eac25d173d21fe3837 Mon Sep 17 00:00:00 2001 From: derselbst Date: Mon, 13 Apr 2026 23:02:49 +0200 Subject: [PATCH 06/14] docs --- include/fluidsynth/voice.h | 34 +++++++++++++++++++++------------- src/synth/fluid_voice.c | 16 ++-------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/include/fluidsynth/voice.h b/include/fluidsynth/voice.h index daa36c7fe..f867e13e2 100644 --- a/include/fluidsynth/voice.h +++ b/include/fluidsynth/voice.h @@ -53,15 +53,24 @@ enum fluid_voice_add_mod /** * Enum indicating the reason a voice callback was invoked. * - * @since 2.5.4 + * @since 2.6.0 */ enum fluid_voice_callback_reason { - FLUID_VOICE_CALLBACK_NOTEOFF, /**< A true noteoff was processed for this voice, i.e. the voice - is neither sustained nor sostenutoed and has entered - its release phase. */ - FLUID_VOICE_CALLBACK_FINISHED /**< The voice has finished playing and is about to be - removed from the DSP loop. */ + /** + * A true noteoff is about to be processed for this voice by the next rendering call, i.e. the voice + * is neither sustained nor sostenutoed and is about to enter its release phase. + * @note This event may not be fired if the sample ends before the voice receives a noteoff event. + * Think of short and unlooped percussion samples, for example. + */ + FLUID_VOICE_CALLBACK_NOTEOFF, + /** + * The voice has finished playing and is about to be + * removed from the DSP loop. The voice remains valid until the callback returns. After that, + * the voice instance should be considered invalid as it may be reclaimed immediately afterwards. + * @note This event will always be fired, even when the voice is being killed or stolen due to polyphony overflow. + */ + FLUID_VOICE_CALLBACK_FINISHED }; /** @@ -71,15 +80,14 @@ enum fluid_voice_callback_reason * @param reason The reason why the callback was invoked (see #fluid_voice_callback_reason). * @param data User-defined data pointer as passed to fluid_voice_set_callback(). * - * @note This callback is invoked from the synthesis thread context. - * The callback implementation must not call any FluidSynth API function - * that could trigger voice or synth modifications, as this may lead to - * deadlocks or data corruption. The callback should be kept as short as - * possible. + * @note It is unspecified from which thread the callback is called. However, the callback may be invoked from the synthesis context. + * In this case, audio synthesis will be blocked until the callback returns. It is therefore highly recommended to + * keep the callback code short, efficient and non-blocking. In realtime-rendering scenarios it is particularly + * discouraged to call any public API functions of the synth or the sequencer from within the callback, as this may acquire a mutex. * - * @since 2.5.4 + * @since 2.6.0 */ -typedef void (*fluid_voice_callback_t)(fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data); +typedef void (*fluid_voice_callback_t)(const fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data); FLUIDSYNTH_API void fluid_voice_add_mod(fluid_voice_t *voice, fluid_mod_t *mod, int mode); FLUIDSYNTH_API float fluid_voice_gen_get(fluid_voice_t *voice, int gen); diff --git a/src/synth/fluid_voice.c b/src/synth/fluid_voice.c index 04614f012..80ceac456 100644 --- a/src/synth/fluid_voice.c +++ b/src/synth/fluid_voice.c @@ -1654,13 +1654,6 @@ int fluid_voice_is_sostenuto(const fluid_voice_t *voice) /** * Set a callback function for a voice to be notified about voice state changes. * - * The callback is invoked when: - * - A true noteoff is processed for this voice (i.e. the voice enters the release - * phase and is neither sustained nor sostenutoed), with reason - * #FLUID_VOICE_CALLBACK_NOTEOFF. - * - The voice has finished playing and is about to be removed from the DSP loop, - * with reason #FLUID_VOICE_CALLBACK_FINISHED. - * * Only one callback function can be registered per voice. Setting a new callback * replaces the previous one. Passing NULL as the callback removes any previously * registered callback. @@ -1673,14 +1666,9 @@ int fluid_voice_is_sostenuto(const fluid_voice_t *voice) * @param data User-defined data pointer passed to the callback. * * @note This function should be called after fluid_synth_alloc_voice() and before - * fluid_synth_start_voice(). - * - * @warning The callback is invoked from the synthesis thread context. - * The callback implementation must not call any FluidSynth API function - * that could trigger voice or synth modifications, as this may lead to - * deadlocks or data corruption. + * fluid_synth_start_voice() to be guaranteed to receive the callback. * - * @since 2.5.4 + * @since 2.6.0 */ void fluid_voice_set_callback(fluid_voice_t *voice, fluid_voice_callback_t callback, void *data) { From 97971ace4440a1223ad6f4aced0956f799624812 Mon Sep 17 00:00:00 2001 From: derselbst Date: Mon, 13 Apr 2026 23:04:52 +0200 Subject: [PATCH 07/14] docs --- include/fluidsynth/voice.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/fluidsynth/voice.h b/include/fluidsynth/voice.h index f867e13e2..b092dc5bc 100644 --- a/include/fluidsynth/voice.h +++ b/include/fluidsynth/voice.h @@ -59,7 +59,7 @@ enum fluid_voice_callback_reason { /** * A true noteoff is about to be processed for this voice by the next rendering call, i.e. the voice - * is neither sustained nor sostenutoed and is about to enter its release phase. + * is neither sustained nor sostenutoed and is about to enter its release phase with the next rendering call. * @note This event may not be fired if the sample ends before the voice receives a noteoff event. * Think of short and unlooped percussion samples, for example. */ From 5a2f6e33ece8c92fb23b05ceb70b2ff511ea03c8 Mon Sep 17 00:00:00 2001 From: derselbst Date: Mon, 13 Apr 2026 23:23:09 +0200 Subject: [PATCH 08/14] oops --- test/test_voice_callback.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_voice_callback.c b/test/test_voice_callback.c index 0bad55346..062b4dc92 100644 --- a/test/test_voice_callback.c +++ b/test/test_voice_callback.c @@ -12,7 +12,7 @@ static void *finished_data_received = NULL; static unsigned int noteoff_voice_id = 0; static unsigned int finished_voice_id = 0; -static void voice_callback(fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data) +static void voice_callback(const fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data) { if(reason == FLUID_VOICE_CALLBACK_NOTEOFF) { From 0d265c57bcdda5e3644ca995fbf5e080963108a5 Mon Sep 17 00:00:00 2001 From: derselbst Date: Sat, 18 Apr 2026 20:29:10 +0200 Subject: [PATCH 09/14] relocate finished callback --- src/rvoice/fluid_rvoice_event.c | 8 ++++++++ src/rvoice/fluid_rvoice_mixer.c | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/rvoice/fluid_rvoice_event.c b/src/rvoice/fluid_rvoice_event.c index f2eef4ad0..4f0710119 100644 --- a/src/rvoice/fluid_rvoice_event.c +++ b/src/rvoice/fluid_rvoice_event.c @@ -102,6 +102,14 @@ fluid_rvoice_eventhandler_finished_voice_callback(fluid_rvoice_eventhandler_t *e { fluid_rvoice_t **vptr = fluid_ringbuffer_get_inptr(eventhandler->finished_voices, 0); + /* Fire user-registered finished callback from the render thread, + * before the voice is pushed to the finished_voices ringbuffer. */ + if(rvoice->finished_cb != NULL) + { + rvoice->finished_cb(rvoice->finished_cb_voice, FLUID_VOICE_CALLBACK_FINISHED, rvoice->finished_cb_data); + rvoice->finished_cb = NULL; + } + if(vptr == NULL) { return; // Buffer full diff --git a/src/rvoice/fluid_rvoice_mixer.c b/src/rvoice/fluid_rvoice_mixer.c index ab022c67c..985383d68 100644 --- a/src/rvoice/fluid_rvoice_mixer.c +++ b/src/rvoice/fluid_rvoice_mixer.c @@ -391,13 +391,6 @@ fluid_mixer_buffer_process_finished_voices(fluid_mixer_buffers_t *buffers) buffers->mixer->active_voices = av; - /* Fire user-registered finished callback from the render thread, - * before the voice is pushed to the finished_voices ringbuffer. */ - if(v->finished_cb != NULL) - { - v->finished_cb(v->finished_cb_voice, 1 /* FLUID_VOICE_CALLBACK_FINISHED */, v->finished_cb_data); - } - fluid_rvoice_eventhandler_finished_voice_callback(buffers->mixer->eventhandler, v); } From 2ac9b6ba4debe3b425a6422d7fb15093e4dbbe7e Mon Sep 17 00:00:00 2001 From: derselbst Date: Sat, 18 Apr 2026 20:31:52 +0200 Subject: [PATCH 10/14] log --- src/rvoice/fluid_rvoice_event.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rvoice/fluid_rvoice_event.c b/src/rvoice/fluid_rvoice_event.c index 4f0710119..7db875c85 100644 --- a/src/rvoice/fluid_rvoice_event.c +++ b/src/rvoice/fluid_rvoice_event.c @@ -112,6 +112,7 @@ fluid_rvoice_eventhandler_finished_voice_callback(fluid_rvoice_eventhandler_t *e if(vptr == NULL) { + FLUID_LOG(FLUID_PANIC, "THIS SHOULD NEVER HAPPEN: eventhandler->finished_voices ringbuffer full!"); return; // Buffer full } From d747dee0e148b782eae3aea8490c0a260463864e Mon Sep 17 00:00:00 2001 From: derselbst Date: Sat, 18 Apr 2026 20:43:40 +0200 Subject: [PATCH 11/14] docs --- doc/recent_changes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/recent_changes.txt b/doc/recent_changes.txt index 2e93656fc..43aff7b89 100644 --- a/doc/recent_changes.txt +++ b/doc/recent_changes.txt @@ -4,6 +4,7 @@ \section NewIn2_6_0 What's new in 2.6.0? - A lookahead limiter has been added, see related setting synth.limiter.* - Support for 24bit and 32bit audio has been added, see fluid_synth_write_s24() and fluid_synth_write_s32() +- Added fluid_voice_set_callback() for voice lifecycle notifications \section NewIn2_5_0 What's new in 2.5.0? - #FLUID_MOD_SIN is now deprecated, use the newly added fluid_mod_set_custom_mapping() From 683e8f31e95d5dc9359c05d93c7e701dfc22de32 Mon Sep 17 00:00:00 2001 From: derselbst Date: Sat, 18 Apr 2026 21:08:36 +0200 Subject: [PATCH 12/14] remove pointless fluid_rvoice_finished_cb_t --- src/rvoice/fluid_rvoice.c | 2 +- src/rvoice/fluid_rvoice.h | 12 +----------- src/synth/fluid_voice.c | 3 +-- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/rvoice/fluid_rvoice.c b/src/rvoice/fluid_rvoice.c index dd168e903..44293cc92 100644 --- a/src/rvoice/fluid_rvoice.c +++ b/src/rvoice/fluid_rvoice.c @@ -955,7 +955,7 @@ DECLARE_FLUID_RVOICE_FUNCTION(fluid_rvoice_set_finished_callback) { fluid_rvoice_t *voice = obj; - voice->finished_cb = (fluid_rvoice_finished_cb_t) param[0].ptr; + voice->finished_cb = (fluid_voice_callback_t) param[0].ptr; voice->finished_cb_voice = param[1].ptr; voice->finished_cb_data = param[2].ptr; } diff --git a/src/rvoice/fluid_rvoice.h b/src/rvoice/fluid_rvoice.h index 5bfd7b431..dd7670dd6 100644 --- a/src/rvoice/fluid_rvoice.h +++ b/src/rvoice/fluid_rvoice.h @@ -153,16 +153,6 @@ struct _fluid_rvoice_buffers_t } bufs[FLUID_RVOICE_MAX_BUFS]; }; - -/** - * Generic callback for rvoice finished notification. - * Invoked from the render thread when the rvoice completes. - * @param voice Opaque pointer (typically the fluid_voice_t*) - * @param reason Integer reason code (FLUID_VOICE_CALLBACK_FINISHED) - * @param data User-defined data pointer - */ -typedef void (*fluid_rvoice_finished_cb_t)(void *voice, int reason, void *data); - /* * Hard realtime parameters needed to synthesize a voice */ @@ -176,7 +166,7 @@ struct _fluid_rvoice_t /* Finished callback, invoked from the render thread when the rvoice * finishes and is about to be removed from the mixer's active list. */ - fluid_rvoice_finished_cb_t finished_cb; + fluid_voice_callback_t finished_cb; void *finished_cb_voice; /* fluid_voice_t* passed as first arg */ void *finished_cb_data; /* user data passed as second arg */ }; diff --git a/src/synth/fluid_voice.c b/src/synth/fluid_voice.c index 80ceac456..269c61a22 100644 --- a/src/synth/fluid_voice.c +++ b/src/synth/fluid_voice.c @@ -1682,8 +1682,7 @@ void fluid_voice_set_callback(fluid_voice_t *voice, fluid_voice_callback_t callb /* Propagate to the rvoice so the finished callback fires from the * render thread immediately when the voice finishes, rather than * being deferred to the next API call. - * The user's fluid_voice_callback_t is ABI-compatible with - * fluid_rvoice_finished_cb_t (void*, int, void*). */ + */ param[0].ptr = (void *)callback; param[1].ptr = voice; param[2].ptr = data; From 68271c8726f9fa37313ab5304bd905e59baa1b27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:24:32 +0000 Subject: [PATCH 13/14] refactor(test): reduce code duplication in test_voice_callback.c Agent-Logs-Url: https://github.com/FluidSynth/fluidsynth/sessions/8f0c89cb-66e9-4e4e-97bc-870c80534532 Co-authored-by: derselbst <8152480+derselbst@users.noreply.github.com> --- test/test_voice_callback.c | 171 ++++++++++++------------------------- 1 file changed, 55 insertions(+), 116 deletions(-) diff --git a/test/test_voice_callback.c b/test/test_voice_callback.c index 062b4dc92..a6724b33c 100644 --- a/test/test_voice_callback.c +++ b/test/test_voice_callback.c @@ -9,8 +9,6 @@ static int noteoff_count = 0; static int finished_count = 0; static void *noteoff_data_received = NULL; static void *finished_data_received = NULL; -static unsigned int noteoff_voice_id = 0; -static unsigned int finished_voice_id = 0; static void voice_callback(const fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data) { @@ -18,13 +16,11 @@ static void voice_callback(const fluid_voice_t *voice, enum fluid_voice_callback { noteoff_count++; noteoff_data_received = data; - noteoff_voice_id = fluid_voice_get_id(voice); } else if(reason == FLUID_VOICE_CALLBACK_FINISHED) { finished_count++; finished_data_received = data; - finished_voice_id = fluid_voice_get_id(voice); } } @@ -34,6 +30,49 @@ static void render_frames(fluid_synth_t *synth, int frames) fluid_synth_process(synth, frames, 0, NULL, 0, NULL); } +/* Reset callback counters and received-data pointers */ +static void reset_callback_state(void) +{ + noteoff_count = 0; + finished_count = 0; + noteoff_data_received = NULL; + finished_data_received = NULL; +} + +/* + * Find all playing voices on the given channel and key, apply a callback + * (may be NULL to clear it) with the given user data, and configure instant + * release so tests finish quickly. Returns the number of voices configured. + */ +static int setup_voices(fluid_synth_t *synth, int chan, int key, + fluid_voice_callback_t callback, void *callback_data) +{ + fluid_voice_t *voices[Polyphony]; + int i; + int voice_count = 0; + + fluid_synth_get_voicelist(synth, voices, Polyphony, -1); + + for(i = 0; i < Polyphony; i++) + { + if(voices[i] == NULL) + { + break; + } + if(fluid_voice_get_key(voices[i]) == key && + fluid_voice_get_channel(voices[i]) == chan && + fluid_voice_is_playing(voices[i])) + { + fluid_voice_set_callback(voices[i], callback, callback_data); + fluid_voice_gen_set(voices[i], GEN_VOLENVRELEASE, -32768); + fluid_voice_update_param(voices[i], GEN_VOLENVRELEASE); + voice_count++; + } + } + + return voice_count; +} + int main(void) { fluid_settings_t *settings; @@ -57,96 +96,32 @@ int main(void) TEST_ASSERT(sfont_id != FLUID_FAILED); /* === Test 1: Callback receives noteoff and finished events === */ - noteoff_count = 0; - finished_count = 0; - noteoff_data_received = NULL; - finished_data_received = NULL; + reset_callback_state(); - /* Play a note using the public MIDI API (noteon) */ TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 100)); - - /* Render a small amount to let the voice start */ render_frames(synth, fluid_synth_get_internal_bufsize(synth)); - /* Now set the callback on all playing voices. - * Since we used noteon, we look for voices on channel 0, key 60. */ - { - int i; - fluid_voice_t *voices[Polyphony]; - int voice_count = 0; - - /* Use fluid_synth_get_voicelist to find the active voice */ - fluid_synth_get_voicelist(synth, voices, Polyphony, -1); - - for(i = 0; i < Polyphony; i++) - { - if(voices[i] == NULL) - { - break; - } - if(fluid_voice_get_key(voices[i]) == 60 && - fluid_voice_get_channel(voices[i]) == 0 && - fluid_voice_is_playing(voices[i])) - { - fluid_voice_set_callback(voices[i], voice_callback, &marker); - // Set the voice to instant release - fluid_voice_gen_set(voices[i], GEN_VOLENVRELEASE, -32768); - fluid_voice_update_param(voices[i], GEN_VOLENVRELEASE); - voice_count++; - } - } - TEST_ASSERT(voice_count > 0); - } + TEST_ASSERT(setup_voices(synth, 0, 60, voice_callback, &marker) > 0); - /* Send noteoff */ TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60)); - /* The noteoff callback should have been invoked at least once */ TEST_ASSERT(noteoff_count > 0); TEST_ASSERT(noteoff_data_received == &marker); - /* Render a large number of frames to let the voice finish its release phase. - * Use all_sounds_off to force immediate release if needed. */ render_frames(synth, 44100 * 2); - /* The finished callback should have been invoked */ TEST_ASSERT(finished_count > 0); TEST_ASSERT(finished_data_received == &marker); /* === Test 2: Removing callback with NULL === */ - noteoff_count = 0; - finished_count = 0; + reset_callback_state(); TEST_SUCCESS(fluid_synth_noteon(synth, 0, 64, 100)); render_frames(synth, fluid_synth_get_internal_bufsize(synth)); - { - fluid_voice_t *voices[Polyphony]; - int i; - int voice_count = 0; - - fluid_synth_get_voicelist(synth, voices, Polyphony, -1); - for(i = 0; i < Polyphony; i++) - { - if(voices[i] == NULL) - { - break; - } - if(fluid_voice_get_key(voices[i]) == 64 && - fluid_voice_get_channel(voices[i]) == 0 && - fluid_voice_is_playing(voices[i])) - { - /* Set callback then remove it */ - fluid_voice_set_callback(voices[i], voice_callback, &marker); - fluid_voice_set_callback(voices[i], NULL, NULL); - // Set the voice to instant release - fluid_voice_gen_set(voices[i], GEN_VOLENVRELEASE, -32768); - fluid_voice_update_param(voices[i], GEN_VOLENVRELEASE); - voice_count++; - } - } - TEST_ASSERT(voice_count > 0); - } + /* Set callback then immediately remove it */ + TEST_ASSERT(setup_voices(synth, 0, 64, voice_callback, &marker) > 0); + TEST_ASSERT(setup_voices(synth, 0, 64, NULL, NULL) > 0); TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 64)); render_frames(synth, fluid_synth_get_internal_bufsize(synth)); @@ -157,70 +132,34 @@ int main(void) TEST_ASSERT(noteoff_count == 0); TEST_ASSERT(finished_count == 0); - /* === Test 3: Same as test 1, but activate sustain to defer noteOff callback === */ - noteoff_count = 0; - finished_count = 0; - noteoff_data_received = NULL; - finished_data_received = NULL; + /* === Test 3: Sustain pedal defers the noteoff callback === */ + reset_callback_state(); - /* Play a note using the public MIDI API (noteon) */ TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 100)); - - /* Render a small amount to let the voice start */ render_frames(synth, fluid_synth_get_internal_bufsize(synth)); - { - int i; - fluid_voice_t *voices[Polyphony]; - int voice_count = 0; - - /* Use fluid_synth_get_voicelist to find the active voice */ - fluid_synth_get_voicelist(synth, voices, Polyphony, -1); - - for(i = 0; i < Polyphony; i++) - { - if(voices[i] == NULL) - { - break; - } - if(fluid_voice_get_key(voices[i]) == 60 && - fluid_voice_get_channel(voices[i]) == 0 && - fluid_voice_is_playing(voices[i])) - { - fluid_voice_set_callback(voices[i], voice_callback, &marker); - // Set the voice to instant release - fluid_voice_gen_set(voices[i], GEN_VOLENVRELEASE, -32768); - fluid_voice_update_param(voices[i], GEN_VOLENVRELEASE); - voice_count++; - } - } - TEST_ASSERT(voice_count > 0); - } + TEST_ASSERT(setup_voices(synth, 0, 60, voice_callback, &marker) > 0); fluid_synth_cc(synth, 0, SUSTAIN_SWITCH, 127); - /* Send noteoff */ TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60)); - // no callback yet because sustain is on + /* No callback yet — sustain is holding the voice */ TEST_ASSERT(noteoff_count == 0); - // render some frames render_frames(synth, fluid_synth_get_internal_bufsize(synth)); - // still no callback + /* Still no callback */ TEST_ASSERT(noteoff_count == 0); TEST_ASSERT(noteoff_data_received == NULL); + /* Releasing sustain triggers the deferred noteoff */ fluid_synth_cc(synth, 0, SUSTAIN_SWITCH, 0); TEST_ASSERT(noteoff_count > 0); TEST_ASSERT(noteoff_data_received == &marker); - /* Render a large number of frames to let the voice finish its release phase. - * Use all_sounds_off to force immediate release if needed. */ render_frames(synth, 44100 * 2); - /* The finished callback should have been invoked */ TEST_ASSERT(finished_count > 0); TEST_ASSERT(finished_data_received == &marker); From d1df0fe3c45df2311a36fc74f982777428e76ed0 Mon Sep 17 00:00:00 2001 From: derselbst Date: Sun, 24 May 2026 12:45:05 +0200 Subject: [PATCH 14/14] replace enum param with int --- include/fluidsynth/voice.h | 2 +- test/test_voice_callback.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/fluidsynth/voice.h b/include/fluidsynth/voice.h index b092dc5bc..550ed8e25 100644 --- a/include/fluidsynth/voice.h +++ b/include/fluidsynth/voice.h @@ -87,7 +87,7 @@ enum fluid_voice_callback_reason * * @since 2.6.0 */ -typedef void (*fluid_voice_callback_t)(const fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data); +typedef void (*fluid_voice_callback_t)(const fluid_voice_t *voice, int reason, void *data); FLUIDSYNTH_API void fluid_voice_add_mod(fluid_voice_t *voice, fluid_mod_t *mod, int mode); FLUIDSYNTH_API float fluid_voice_gen_get(fluid_voice_t *voice, int gen); diff --git a/test/test_voice_callback.c b/test/test_voice_callback.c index a6724b33c..d16e176f7 100644 --- a/test/test_voice_callback.c +++ b/test/test_voice_callback.c @@ -10,7 +10,7 @@ static int finished_count = 0; static void *noteoff_data_received = NULL; static void *finished_data_received = NULL; -static void voice_callback(const fluid_voice_t *voice, enum fluid_voice_callback_reason reason, void *data) +static void voice_callback(const fluid_voice_t *voice, int reason, void *data) { if(reason == FLUID_VOICE_CALLBACK_NOTEOFF) {