From 5c1cfe6a5f9be9e7fde2737feb0628ba983541e2 Mon Sep 17 00:00:00 2001 From: Tom M Date: Sun, 3 Jan 2021 09:42:42 +0100 Subject: [PATCH] Regression tests for #727 (#735) This PR adds regression tests for #727, ensuring that soundfonts are correctly unloaded via the lazy-timer-unloading mechanism. --- src/synth/fluid_synth.c | 15 ++- src/synth/fluid_synth.h | 1 + src/utils/fluid_sys.c | 18 +++ src/utils/fluid_sys.h | 2 + test/CMakeLists.txt | 5 +- test/test_sfont_unloading.c | 229 ++++++++++++++++++++++++++++++++++++ 6 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 test/test_sfont_unloading.c diff --git a/src/synth/fluid_synth.c b/src/synth/fluid_synth.c index 3a900a5f..991ef972 100644 --- a/src/synth/fluid_synth.c +++ b/src/synth/fluid_synth.c @@ -1119,6 +1119,18 @@ delete_fluid_synth(fluid_synth_t *synth) delete_fluid_list(synth->loaders); + /* wait for and delete all the lazy sfont unloading timers */ + + for(list = synth->fonts_to_be_unloaded; list; list = fluid_list_next(list)) + { + fluid_timer_t* timer = fluid_list_get(list); + // explicitly join to wait for the unload really to happen + fluid_timer_join(timer); + // delete_fluid_timer alone would stop the timer, even if it had not unloaded the soundfont yet + delete_fluid_timer(timer); + } + + delete_fluid_list(synth->fonts_to_be_unloaded); if(synth->channel != NULL) { @@ -4846,7 +4858,8 @@ fluid_synth_sfont_unref(fluid_synth_t *synth, fluid_sfont_t *sfont) } /* spin off a timer thread to unload the sfont later (SoundFont loader blocked unload) */ else { - new_fluid_timer(100, fluid_synth_sfunload_callback, sfont, TRUE, TRUE, FALSE); + fluid_timer_t* timer = new_fluid_timer(100, fluid_synth_sfunload_callback, sfont, TRUE, FALSE, FALSE); + synth->fonts_to_be_unloaded = fluid_list_prepend(synth->fonts_to_be_unloaded, timer); } } } diff --git a/src/synth/fluid_synth.h b/src/synth/fluid_synth.h index b649bcf3..59980dd8 100644 --- a/src/synth/fluid_synth.h +++ b/src/synth/fluid_synth.h @@ -127,6 +127,7 @@ struct _fluid_synth_t fluid_list_t *loaders; /**< the SoundFont loaders */ fluid_list_t *sfont; /**< List of fluid_sfont_info_t for each loaded SoundFont (remains until SoundFont is unloaded) */ int sfont_id; /**< Incrementing ID assigned to each loaded SoundFont */ + fluid_list_t *fonts_to_be_unloaded; /**< list of timers that try to unload a soundfont */ float gain; /**< master gain */ fluid_channel_t **channel; /**< the channels */ diff --git a/src/utils/fluid_sys.c b/src/utils/fluid_sys.c index 3f2687a9..9da394e7 100644 --- a/src/utils/fluid_sys.c +++ b/src/utils/fluid_sys.c @@ -60,6 +60,10 @@ typedef struct struct _fluid_timer_t { long msec; + + // Pointer to a function to be executed by the timer. + // This field is set to NULL once the timer is finished to indicate completion. + // This allows for timed waits, rather than waiting forever as fluid_timer_join() does. fluid_timer_callback_t callback; void *data; fluid_thread_t *thread; @@ -1096,6 +1100,7 @@ fluid_timer_run(void *data) } FLUID_LOG(FLUID_DBG, "Timer thread finished"); + timer->callback = NULL; if(timer->auto_destroy) { @@ -1189,6 +1194,19 @@ fluid_timer_join(fluid_timer_t *timer) return FLUID_OK; } +int +fluid_timer_is_running(const fluid_timer_t *timer) +{ + // for unit test usage only + return timer->callback != NULL; +} + +long fluid_timer_get_interval(const fluid_timer_t * timer) +{ + // for unit test usage only + return timer->msec; +} + /*************************************************************** * diff --git a/src/utils/fluid_sys.h b/src/utils/fluid_sys.h index c1644035..7c7db764 100644 --- a/src/utils/fluid_sys.h +++ b/src/utils/fluid_sys.h @@ -232,6 +232,8 @@ fluid_timer_t *new_fluid_timer(int msec, fluid_timer_callback_t callback, void delete_fluid_timer(fluid_timer_t *timer); int fluid_timer_join(fluid_timer_t *timer); int fluid_timer_stop(fluid_timer_t *timer); +int fluid_timer_is_running(const fluid_timer_t *timer); +long fluid_timer_get_interval(const fluid_timer_t * timer); // Macros to use for pre-processor if statements to test which Glib thread API we have (pre or post 2.32) #define NEW_GLIB_THREAD_API GLIB_CHECK_VERSION(2,32,0) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 42b68fdd..43a54852 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,9 +20,10 @@ ADD_FLUID_TEST(test_snprintf) ADD_FLUID_TEST(test_synth_process) ADD_FLUID_TEST(test_ct2hz) ADD_FLUID_TEST(test_sample_validate) -ADD_FLUID_TEST(test_seq_event_queue_sort) -ADD_FLUID_TEST(test_seq_scale) ADD_FLUID_TEST(test_jack_obtaining_synth) +ADD_FLUID_TEST(test_sfont_unloading) +ADD_FLUID_TEST(test_seq_scale) +ADD_FLUID_TEST(test_seq_event_queue_sort) # if ( LIBSNDFILE_HASVORBIS ) # ADD_FLUID_TEST(test_sf3_sfont_loading) diff --git a/test/test_sfont_unloading.c b/test/test_sfont_unloading.c new file mode 100644 index 00000000..922a0527 --- /dev/null +++ b/test/test_sfont_unloading.c @@ -0,0 +1,229 @@ + +#include "test.h" +#include "fluidsynth.h" +#include "synth/fluid_synth.h" +#include "utils/fluid_sys.h" + +void wait_and_free(fluid_synth_t* synth, int id, const char* calling_func) +{ + fluid_list_t *list, *list_orig; + list_orig = list = synth->fonts_to_be_unloaded; + synth->fonts_to_be_unloaded = NULL; + delete_fluid_synth(synth); + + for(; list; list = fluid_list_next(list)) + { + fluid_timer_t* timer = fluid_list_get(list); + FLUID_LOG(FLUID_INFO, "%s(): Start waiting for soundfont %d to unload", calling_func, id); + if(fluid_timer_is_running(timer)) + { + /* timer still running, wait a bit*/ + fluid_msleep(50 * fluid_timer_get_interval(timer)); + TEST_ASSERT(!fluid_timer_is_running(timer)); + } + delete_fluid_timer(timer); + FLUID_LOG(FLUID_INFO, "%s(): End waiting for soundfont %d to unload", calling_func, id); + } + delete_fluid_list(list_orig); +} + +static void test_without_rendering(fluid_settings_t* settings) +{ + int id; + fluid_synth_t *synth = new_fluid_synth(settings); + TEST_ASSERT(synth != NULL); + + TEST_ASSERT(fluid_is_soundfont(TEST_SOUNDFONT) == TRUE); + + // load a sfont to synth + TEST_SUCCESS(id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 1)); + // one sfont loaded + TEST_ASSERT(fluid_synth_sfcount(synth) == 1); + + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 127)); + + TEST_SUCCESS(fluid_synth_sfunload(synth, id, 1)); + + TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60)); + + // there must be one font scheduled for lazy unloading + TEST_ASSERT(synth->fonts_to_be_unloaded != NULL); + + wait_and_free(synth, id, __func__); +} + +// this should work fine after applying JJCs fix a4ac56502fec5f0c20a60187d965c94ba1dc81c2 +static void test_after_polyphony_exceeded(fluid_settings_t* settings) +{ + int id; + fluid_synth_t *synth = new_fluid_synth(settings); + TEST_ASSERT(synth != NULL); + + TEST_ASSERT(fluid_is_soundfont(TEST_SOUNDFONT) == TRUE); + + // load a sfont to synth + TEST_SUCCESS(id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 1)); + // one sfont loaded + TEST_ASSERT(fluid_synth_sfcount(synth) == 1); + + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 127)); + FLUID_LOG(FLUID_INFO, "test_after_polyphony_exceeded(): note on C4, voice count=%d", + fluid_synth_get_active_voice_count(synth)); + + // need to render a bit to make synth->ticks_since_start advance, to make the previous voice "killable" + TEST_SUCCESS(fluid_synth_process(synth, 2048, 0, NULL, 0, NULL)); + + // polyphony exceeded - killing the killable voice from above + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 61, 127)); + + // need to render again, to make the synth thread assign rvoice->dsp.sample, so that sample_unref() later really unrefs + TEST_SUCCESS(fluid_synth_process(synth, 2048, 0, NULL, 0, NULL)); + FLUID_LOG(FLUID_INFO, "test_after_polyphony_exceeded(): note on C#4, voice count=%d", + fluid_synth_get_active_voice_count(synth)); + + FLUID_LOG(FLUID_INFO, "test_after_polyphony_exceeded(): unload sounfont"); + TEST_SUCCESS(fluid_synth_sfunload(synth, id, 1)); + + TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 61)); + + // need to render yet again, to make the synth thread release the rvoice so it can be reclaimed by + // fluid_synth_check_finished_voices() + // need to render may more samples this time, so the voice makes it pass the release phase... + TEST_SUCCESS(fluid_synth_process(synth, 204800, 0, NULL, 0, NULL)); + + // make any API call to execute fluid_synth_check_finished_voices() + FLUID_LOG(FLUID_INFO, "test_after_polyphony_exceeded(): note off C#4, voice count=%d", + fluid_synth_get_active_voice_count(synth)); + + // there must be one font scheduled for lazy unloading + TEST_ASSERT(synth->fonts_to_be_unloaded != NULL); + + wait_and_free(synth, id, __func__); +} + +static void test_default_polyphony(fluid_settings_t* settings, int with_rendering) +{ + enum { BUFSIZE = 128 }; + fluid_voice_t* buf[BUFSIZE]; + + int id; + fluid_synth_t *synth = new_fluid_synth(settings); + TEST_ASSERT(synth != NULL); + + TEST_ASSERT(fluid_is_soundfont(TEST_SOUNDFONT) == TRUE); + + // load a sfont to synth + TEST_SUCCESS(id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 1)); + // one sfont loaded + TEST_ASSERT(fluid_synth_sfcount(synth) == 1); + + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 60, 127)); + + if(with_rendering) + { + TEST_SUCCESS(fluid_synth_process(synth, fluid_synth_get_internal_bufsize(synth), 0, NULL, 0, NULL)); + } + + TEST_SUCCESS(fluid_synth_noteon(synth, 0, 61, 127)); + + fluid_synth_get_voicelist(synth, buf, BUFSIZE, -1); + + TEST_ASSERT(fluid_synth_get_active_voice_count(synth) == 4); + + if(with_rendering) + { + // make the synth thread assign rvoice->dsp.sample + TEST_SUCCESS(fluid_synth_process(synth, 2 * fluid_synth_get_internal_bufsize(synth), 0, NULL, 0, NULL)); + + TEST_ASSERT(fluid_synth_get_active_voice_count(synth) == 4); + } + + TEST_ASSERT(synth->fonts_to_be_unloaded == NULL); + + TEST_SUCCESS(fluid_synth_sfunload(synth, id, 1)); + + // now, there must be one font scheduled for lazy unloading + TEST_ASSERT(synth->fonts_to_be_unloaded != NULL); + TEST_ASSERT(fluid_timer_is_running(fluid_list_get(synth->fonts_to_be_unloaded))); + + // noteoff the second note and render something + TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 61)); + if(with_rendering) + { + TEST_SUCCESS(fluid_synth_process(synth, fluid_synth_get_internal_bufsize(synth), 0, NULL, 0, NULL)); + } + + // still 4 because key 61 is playing in release phase now + TEST_ASSERT(fluid_synth_get_active_voice_count(synth) == 4); + // must be still running + TEST_ASSERT(synth->fonts_to_be_unloaded != NULL); + TEST_ASSERT(fluid_timer_is_running(fluid_list_get(synth->fonts_to_be_unloaded))); + + // noteoff the first note and render something + TEST_SUCCESS(fluid_synth_noteoff(synth, 0, 60)); + if(with_rendering) + { + TEST_SUCCESS(fluid_synth_process(synth, fluid_synth_get_internal_bufsize(synth), 0, NULL, 0, NULL)); + } + + // still 4 because keys 60 + 61 are playing in release phase now + TEST_ASSERT(fluid_synth_get_active_voice_count(synth) == 4); + // must be still running + TEST_ASSERT(synth->fonts_to_be_unloaded != NULL); + TEST_ASSERT(fluid_timer_is_running(fluid_list_get(synth->fonts_to_be_unloaded))); + + if(with_rendering) + { + // render enough, to make the synth thread release the rvoice so it can be reclaimed by + // fluid_synth_check_finished_voices() + TEST_SUCCESS(fluid_synth_process(synth, 2048000, 0, NULL, 0, NULL)); + + // this API call should reclaim the rvoices and call fluid_voice_stop() + TEST_ASSERT(fluid_synth_get_active_voice_count(synth) == 0); + } + + TEST_ASSERT(synth->fonts_to_be_unloaded != NULL); + if(with_rendering) + { + // We want to see that the timer thread unloads the soundfont before we call delete_fluid_synth(). + // Wait to give the timer thread a chance to unload and finish. + fluid_msleep(10 * fluid_timer_get_interval(fluid_list_get(synth->fonts_to_be_unloaded))); + TEST_ASSERT(!fluid_timer_is_running(fluid_list_get(synth->fonts_to_be_unloaded))); + } + else + { + TEST_ASSERT(fluid_timer_is_running(fluid_list_get(synth->fonts_to_be_unloaded))); + } + + wait_and_free(synth, id, __func__); +} + +// this tests the soundfont loading API of the synth. +// might be expanded to test the soundfont loader as well... +int main(void) +{ + fluid_settings_t *settings = new_fluid_settings(); + TEST_ASSERT(settings != NULL); + + FLUID_LOG(FLUID_INFO, "Begin test_default_polyphony() with rendering"); + test_default_polyphony(settings, TRUE); + FLUID_LOG(FLUID_INFO, "End test_default_polyphony()\n"); + + FLUID_LOG(FLUID_INFO, "Begin test_default_polyphony() without rendering"); + test_default_polyphony(settings, FALSE); + FLUID_LOG(FLUID_INFO, "End test_default_polyphony()\n"); + + fluid_settings_setint(settings, "synth.polyphony", 2); + + FLUID_LOG(FLUID_INFO, "Begin test_after_polyphony_exceeded()"); + test_after_polyphony_exceeded(settings); + FLUID_LOG(FLUID_INFO, "End test_after_polyphony_exceeded()\n"); + + FLUID_LOG(FLUID_INFO, "Begin test_without_rendering()"); + test_without_rendering(settings); + FLUID_LOG(FLUID_INFO, "End test_without_rendering()"); + + delete_fluid_settings(settings); + + return EXIT_SUCCESS; +}