Skip to content

Add fluid_voice_set_callback() for voice lifecycle notifications#1778

Merged
derselbst merged 17 commits into
masterfrom
add-fluid-voice-callback
May 24, 2026
Merged

Add fluid_voice_set_callback() for voice lifecycle notifications#1778
derselbst merged 17 commits into
masterfrom
add-fluid-voice-callback

Conversation

@derselbst
Copy link
Copy Markdown
Member

@derselbst derselbst commented Apr 18, 2026

Currently, apps using fluid_synth_start() / fluid_synth_start_voice() have no way to know when a fluid_voice_t or its ID becomes invalid — i.e. the voice may finish playing before even receiving a noteoff (e.g. short unlooped percussion samples). This adds a per-voice callback for two lifecycle events: true noteoff (release phase entered, not held by sustain/sostenuto) and voice finished (removed from DSP loop).

Example Usage

void my_cb(const fluid_voice_t *voice, int reason, void *data) {
    unsigned int id = fluid_voice_get_id(voice);
    if ((enum fluid_voice_callback_reason)reason == FLUID_VOICE_CALLBACK_FINISHED) {
        /* voice id is now invalid, remove from tracking */
    }
}

fluid_voice_t *voice = fluid_synth_alloc_voice(synth, sample, chan, key, vel);
fluid_voice_set_callback(voice, my_cb, my_app_data);
fluid_synth_start_voice(synth, voice);

Copilot AI and others added 12 commits April 11, 2026 18:33
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>
…ext 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>
… thread

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>
…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>
@derselbst derselbst added this to the 2.6 milestone Apr 18, 2026
@derselbst
Copy link
Copy Markdown
Member Author

@copilot SonarQube complains about too much code duplication in the newly added test/test_voice_callback.c. Pls. fix that for me, thanks.

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 18, 2026

@copilot SonarQube complains about too much code duplication in the newly added test/test_voice_callback.c. Pls. fix that for me, thanks.

Fixed in 68271c8. The three near-identical voicelist-scan-and-configure blocks are now consolidated into a setup_voices() helper, and the repeated counter/pointer resets are in a reset_callback_state() helper. Also removed the unused noteoff_voice_id/finished_voice_id globals while I was at it.

@sonarqubecloud
Copy link
Copy Markdown

@sonarqubecloud
Copy link
Copy Markdown

@derselbst derselbst merged commit 269788a into master May 24, 2026
72 of 87 checks passed
@derselbst derselbst deleted the add-fluid-voice-callback branch May 24, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants