Skip to content
Merged
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
1 change: 1 addition & 0 deletions doc/recent_changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
\section NewIn2_6_0 What's new in 2.6.0?
- A lookahead limiter has been added, see \setting{synth_limiter_active} and other related limiter settings
- 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_4 What's new in 2.5.4?
- By default, fluidsynth now auto selects the TCP port for the shell server, see \setting{shell_port}
Expand Down
40 changes: 40 additions & 0 deletions include/fluidsynth/voice.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,45 @@
FLUID_VOICE_DEFAULT /**< For default modulators only, no need to check for duplicates */
};

/**
* Enum indicating the reason a voice callback was invoked.
*
* @since 2.6.0
*/
enum fluid_voice_callback_reason

Check warning on line 58 in include/fluidsynth/voice.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this "enum" with "enum class".

See more on https://sonarcloud.io/project/issues?id=FluidSynth_fluidsynth&issues=AZ5ZnKdpm5Gz0TQXupWF&open=AZ5ZnKdpm5Gz0TQXupWF&pullRequest=1778
{
/**
* 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 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.
*/
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
};

/**
* 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 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.6.0
*/
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);
FLUIDSYNTH_API void fluid_voice_gen_set(fluid_voice_t *voice, int gen, float val);
Expand All @@ -67,6 +106,7 @@
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
Expand Down
15 changes: 15 additions & 0 deletions src/rvoice/fluid_rvoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -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? */
Expand Down Expand Up @@ -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_voice_callback_t) param[0].ptr;
voice->finished_cb_voice = param[1].ptr;
voice->finished_cb_data = param[2].ptr;
}


8 changes: 7 additions & 1 deletion src/rvoice/fluid_rvoice.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ struct _fluid_rvoice_buffers_t
} bufs[FLUID_RVOICE_MAX_BUFS];
};


/*
* Hard realtime parameters needed to synthesize a voice
*/
Expand All @@ -164,6 +163,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_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 */
};


Expand Down Expand Up @@ -196,6 +201,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);
Expand Down
9 changes: 9 additions & 0 deletions src/rvoice/fluid_rvoice_event.c
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,17 @@ 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)
{
FLUID_LOG(FLUID_PANIC, "THIS SHOULD NEVER HAPPEN: eventhandler->finished_voices ringbuffer full!");
return; // Buffer full
}

Expand Down
47 changes: 47 additions & 0 deletions src/synth/fluid_voice.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/*
Expand Down Expand Up @@ -1325,6 +1327,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);
}
}

/*
Expand Down Expand Up @@ -1630,6 +1637,46 @@ 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.
*
* 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() to be guaranteed to receive the callback.
*
* @since 2.6.0
*/
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 = (void *)callback;
param[1].ptr = voice;
param[2].ptr = data;
fluid_rvoice_eventhandler_push(voice->eventhandler,
fluid_rvoice_set_finished_callback,
voice->rvoice, param);
}

/**
* Return the MIDI channel the voice is playing on.
*
Expand Down
4 changes: 4 additions & 0 deletions src/synth/fluid_voice.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ADD_FLUID_TEST(test_file_seek_tell)
ADD_FLUID_TEST(test_shell_server_auto_port)
ADD_FLUID_TEST(test_sample_pitch_calculation)
ADD_FLUID_TEST(test_mts_cc_tuning)
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
Expand Down
171 changes: 171 additions & 0 deletions test/test_voice_callback.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@

#include "test.h"
#include "fluidsynth.h"
#include "fluid_midi.h"

enum { Polyphony = 64 };

static int noteoff_count = 0;
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, int reason, void *data)

Check warning on line 13 in test/test_voice_callback.c

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused parameter "voice".

See more on https://sonarcloud.io/project/issues?id=FluidSynth_fluidsynth&issues=AZ5ZnKbvm5Gz0TQXupWE&open=AZ5ZnKbvm5Gz0TQXupWE&pullRequest=1778
{
if(reason == FLUID_VOICE_CALLBACK_NOTEOFF)
{
noteoff_count++;
noteoff_data_received = data;
}
else if(reason == FLUID_VOICE_CALLBACK_FINISHED)
{
finished_count++;
finished_data_received = data;
}
}

/* Render some audio to advance the synth state */
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;
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));

TEST_SUCCESS(fluid_settings_setint(settings, "synth.polyphony", Polyphony));

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 === */
reset_callback_state();

TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 100));
render_frames(synth, fluid_synth_get_internal_bufsize(synth));

TEST_ASSERT(setup_voices(synth, 0, 60, voice_callback, &marker) > 0);

TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60));

TEST_ASSERT(noteoff_count > 0);
TEST_ASSERT(noteoff_data_received == &marker);

render_frames(synth, 44100 * 2);

TEST_ASSERT(finished_count > 0);
TEST_ASSERT(finished_data_received == &marker);

/* === Test 2: Removing callback with NULL === */
reset_callback_state();

TEST_SUCCESS(fluid_synth_noteon(synth, 0, 64, 100));
render_frames(synth, fluid_synth_get_internal_bufsize(synth));

/* 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));
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: Sustain pedal defers the noteoff callback === */
reset_callback_state();

TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 100));
render_frames(synth, fluid_synth_get_internal_bufsize(synth));

TEST_ASSERT(setup_voices(synth, 0, 60, voice_callback, &marker) > 0);

fluid_synth_cc(synth, 0, SUSTAIN_SWITCH, 127);

TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60));

/* No callback yet — sustain is holding the voice */
TEST_ASSERT(noteoff_count == 0);

render_frames(synth, fluid_synth_get_internal_bufsize(synth));

/* 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_frames(synth, 44100 * 2);

TEST_ASSERT(finished_count > 0);
TEST_ASSERT(finished_data_received == &marker);

/* cleanup */
delete_fluid_synth(synth);
delete_fluid_settings(settings);

return EXIT_SUCCESS;
}
Loading