diff --git a/doc/fluidsynth-v20-devdoc.txt b/doc/fluidsynth-v20-devdoc.txt index 84b96769..3222b8ba 100644 --- a/doc/fluidsynth-v20-devdoc.txt +++ b/doc/fluidsynth-v20-devdoc.txt @@ -80,6 +80,7 @@ What is FluidSynth? - The sequencer's queue no longer blocks the synthesizer thread, due to being busy arranging its events internally. - Events that share the same tick was given a new, documented order, see fluid_sequencer_send_at(). - The sequencer's scale can now be used for arbitrary tempo changes. Previously, the scale of the sequencer was limited to 1000. The only limitation now is >0. +- The dynamic-sample-loader has learned support to pin samples, see fluid_synth_pin_preset() and fluid_synth_unpin_preset() \section NewIn2_1_4 What's new in 2.1.4? diff --git a/include/fluidsynth/sfont.h b/include/fluidsynth/sfont.h index 90c88be2..12cae5f7 100644 --- a/include/fluidsynth/sfont.h +++ b/include/fluidsynth/sfont.h @@ -68,7 +68,9 @@ enum { FLUID_PRESET_SELECTED, /**< Preset selected notify */ FLUID_PRESET_UNSELECTED, /**< Preset unselected notify */ - FLUID_SAMPLE_DONE /**< Sample no longer needed notify */ + FLUID_SAMPLE_DONE, /**< Sample no longer needed notify */ + FLUID_PRESET_PIN, /**< Request to pin preset samples to cache */ + FLUID_PRESET_UNPIN /**< Request to unpin preset samples from cache */ }; /** diff --git a/include/fluidsynth/synth.h b/include/fluidsynth/synth.h index ef7f4e2a..55c83da4 100644 --- a/include/fluidsynth/synth.h +++ b/include/fluidsynth/synth.h @@ -84,6 +84,15 @@ FLUIDSYNTH_API int fluid_synth_system_reset(fluid_synth_t *synth); FLUIDSYNTH_API int fluid_synth_all_notes_off(fluid_synth_t *synth, int chan); FLUIDSYNTH_API int fluid_synth_all_sounds_off(fluid_synth_t *synth, int chan); + +/* Dynamic sample loading */ + +FLUIDSYNTH_API +int fluid_synth_pin_preset(fluid_synth_t *synth, int sfont_id, int bank_num, int preset_num); + +FLUIDSYNTH_API +int fluid_synth_unpin_preset(fluid_synth_t *synth, int sfont_id, int bank_num, int preset_num); + /** * The midi channel type used by fluid_synth_set_channel_type() */ diff --git a/src/sfloader/fluid_defsfont.c b/src/sfloader/fluid_defsfont.c index 6a77f4d8..f473db4c 100644 --- a/src/sfloader/fluid_defsfont.c +++ b/src/sfloader/fluid_defsfont.c @@ -34,6 +34,8 @@ #define EMU_ATTENUATION_FACTOR (0.4f) /* Dynamic sample loading functions */ +static int pin_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset); +static int unpin_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset); static int load_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset); static int unload_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset); static void unload_sample(fluid_sample_t *sample); @@ -228,6 +230,17 @@ int delete_fluid_defsfont(fluid_defsfont_t *defsfont) fluid_return_val_if_fail(defsfont != NULL, FLUID_OK); + /* If we use dynamic sample loading, make sure we unpin any + * pinned presets before removing this soundfont */ + if(defsfont->dynamic_samples) + { + for(list = defsfont->preset; list; list = fluid_list_next(list)) + { + preset = (fluid_preset_t *)fluid_list_get(list); + unpin_preset_samples(defsfont, preset); + } + } + /* Check that no samples are currently used */ for(list = defsfont->sample; list; list = fluid_list_next(list)) { @@ -637,6 +650,7 @@ new_fluid_defpreset(void) defpreset->num = 0; defpreset->global_zone = NULL; defpreset->zone = NULL; + defpreset->pinned = FALSE; return defpreset; } @@ -2003,15 +2017,74 @@ static int dynamic_samples_preset_notify(fluid_preset_t *preset, int reason, int { FLUID_LOG(FLUID_DBG, "Selected preset '%s' on channel %d", fluid_preset_get_name(preset), chan); defsfont = fluid_sfont_get_data(preset->sfont); - load_preset_samples(defsfont, preset); + return load_preset_samples(defsfont, preset); } - else if(reason == FLUID_PRESET_UNSELECTED) + + if(reason == FLUID_PRESET_UNSELECTED) { FLUID_LOG(FLUID_DBG, "Deselected preset '%s' from channel %d", fluid_preset_get_name(preset), chan); defsfont = fluid_sfont_get_data(preset->sfont); - unload_preset_samples(defsfont, preset); + return unload_preset_samples(defsfont, preset); } + if(reason == FLUID_PRESET_PIN) + { + defsfont = fluid_sfont_get_data(preset->sfont); + return pin_preset_samples(defsfont, preset); + } + + if(reason == FLUID_PRESET_UNPIN) + { + defsfont = fluid_sfont_get_data(preset->sfont); + return unpin_preset_samples(defsfont, preset); + } + + return FLUID_OK; +} + + +static int pin_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset) +{ + fluid_defpreset_t *defpreset; + + defpreset = fluid_preset_get_data(preset); + if (defpreset->pinned) + { + return FLUID_OK; + } + + FLUID_LOG(FLUID_DBG, "Pinning preset '%s'", fluid_preset_get_name(preset)); + + if(load_preset_samples(defsfont, preset) == FLUID_FAILED) + { + return FLUID_FAILED; + } + + defpreset->pinned = TRUE; + + return FLUID_OK; +} + + +static int unpin_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset) +{ + fluid_defpreset_t *defpreset; + + defpreset = fluid_preset_get_data(preset); + if (!defpreset->pinned) + { + return FLUID_OK; + } + + FLUID_LOG(FLUID_DBG, "Unpinning preset '%s'", fluid_preset_get_name(preset)); + + if(unload_preset_samples(defsfont, preset) == FLUID_FAILED) + { + return FLUID_FAILED; + } + + defpreset->pinned = FALSE; + return FLUID_OK; } diff --git a/src/sfloader/fluid_defsfont.h b/src/sfloader/fluid_defsfont.h index 7f50faa6..fce4eea3 100644 --- a/src/sfloader/fluid_defsfont.h +++ b/src/sfloader/fluid_defsfont.h @@ -148,6 +148,7 @@ struct _fluid_defpreset_t unsigned int num; /* the preset number */ fluid_preset_zone_t *global_zone; /* the global zone of the preset */ fluid_preset_zone_t *zone; /* the chained list of preset zones */ + int pinned; /* preset samples pinned to sample cache? */ }; fluid_defpreset_t *new_fluid_defpreset(void); diff --git a/src/sfloader/fluid_samplecache.c b/src/sfloader/fluid_samplecache.c index 395bf558..2d70d7e8 100644 --- a/src/sfloader/fluid_samplecache.c +++ b/src/sfloader/fluid_samplecache.c @@ -290,3 +290,22 @@ static int fluid_get_file_modification_time(char *filename, time_t *modification *modification_time = buf.st_mtime; return FLUID_OK; } + + +/* Only used for tests */ +int fluid_samplecache_count_entries(void) +{ + fluid_list_t *entry; + int count = 0; + + fluid_mutex_lock(samplecache_mutex); + + for(entry = samplecache_list; entry != NULL; entry = fluid_list_next(entry)) + { + count++; + } + + fluid_mutex_unlock(samplecache_mutex); + + return count; +} diff --git a/src/sfloader/fluid_samplecache.h b/src/sfloader/fluid_samplecache.h index 49b802ba..de6206ba 100644 --- a/src/sfloader/fluid_samplecache.h +++ b/src/sfloader/fluid_samplecache.h @@ -31,4 +31,7 @@ int fluid_samplecache_load(SFData *sf, int fluid_samplecache_unload(const short *sample_data); +/* Only used for tests */ +int fluid_samplecache_count_entries(void); + #endif /* _FLUID_SAMPLECACHE_H */ diff --git a/src/sfloader/fluid_sfont.h b/src/sfloader/fluid_sfont.h index 28609e96..6a480a83 100644 --- a/src/sfloader/fluid_sfont.h +++ b/src/sfloader/fluid_sfont.h @@ -45,7 +45,7 @@ int fluid_sample_sanitize_loop(fluid_sample_t *sample, unsigned int max_end); (*(_preset)->noteon)(_preset,_synth,_ch,_key,_vel) #define fluid_preset_notify(_preset,_reason,_chan) \ - { if ((_preset) && (_preset)->notify) { (*(_preset)->notify)(_preset,_reason,_chan); }} + ( ((_preset) && (_preset)->notify) ? (*(_preset)->notify)(_preset,_reason,_chan) : FLUID_OK ) #define fluid_sample_incr_ref(_sample) { (_sample)->refcount++; } diff --git a/src/synth/fluid_synth.c b/src/synth/fluid_synth.c index 7380ce0f..5aaca720 100644 --- a/src/synth/fluid_synth.c +++ b/src/synth/fluid_synth.c @@ -3018,6 +3018,97 @@ fluid_synth_program_select(fluid_synth_t *synth, int chan, int sfont_id, FLUID_API_RETURN(result); } +/** + * Pins all samples of the given preset. + * + * This function will attempt to pin all samples of the given preset and + * load them into memory, if they are currently unloaded. "To pin" in this + * context means preventing them from being unloaded by an upcoming channel + * prog change. + * + * @note This function is only useful if synth.dynamic-sample-loading is enabled. + * By default, dynamic-sample-loading is disabled and all samples are kept in memory. + * Furthermore, this is only useful for presets which support dynamic-sample-loading (currently, + * only preset loaded with the default soundfont loader do). + * @param synth FluidSynth instance + * @param sfont_id ID of a loaded SoundFont + * @param bank_num MIDI bank number + * @param preset_num MIDI program number + * @return #FLUID_OK if the preset was found, pinned and loaded + * into memory successfully. #FLUID_FAILED otherwise. Note that #FLUID_OK + * is returned, even if synth.dynamic-sample-loading is disabled or + * the preset doesn't support dynamic-sample-loading. + * @since 2.2.0 + */ +int +fluid_synth_pin_preset(fluid_synth_t *synth, int sfont_id, int bank_num, int preset_num) +{ + int ret; + fluid_preset_t *preset; + + fluid_return_val_if_fail(synth != NULL, FLUID_FAILED); + fluid_return_val_if_fail(bank_num >= 0, FLUID_FAILED); + fluid_return_val_if_fail(preset_num >= 0, FLUID_FAILED); + + fluid_synth_api_enter(synth); + + preset = fluid_synth_get_preset(synth, sfont_id, bank_num, preset_num); + + if(preset == NULL) + { + FLUID_LOG(FLUID_ERR, + "There is no preset with bank number %d and preset number %d in SoundFont %d", + bank_num, preset_num, sfont_id); + FLUID_API_RETURN(FLUID_FAILED); + } + + ret = fluid_preset_notify(preset, FLUID_PRESET_PIN, -1); // channel unused for pinning messages + + FLUID_API_RETURN(ret); +} + +/** + * Unpin all samples of the given preset. + * + * This function undoes the effect of fluid_synth_pin_preset(). If the preset is + * not currently used, its samples will be unloaded. + * + * @note Only useful for presets loaded with the default soundfont loader and + * only if synth.dynamic-sample-loading is enabled. + * @param synth FluidSynth instance + * @param sfont_id ID of a loaded SoundFont + * @param bank_num MIDI bank number + * @param preset_num MIDI program number + * @return #FLUID_OK if preset was found, #FLUID_FAILED otherwise + * @since 2.2.0 + */ +int +fluid_synth_unpin_preset(fluid_synth_t *synth, int sfont_id, int bank_num, int preset_num) +{ + int ret; + fluid_preset_t *preset; + + fluid_return_val_if_fail(synth != NULL, FLUID_FAILED); + fluid_return_val_if_fail(bank_num >= 0, FLUID_FAILED); + fluid_return_val_if_fail(preset_num >= 0, FLUID_FAILED); + + fluid_synth_api_enter(synth); + + preset = fluid_synth_get_preset(synth, sfont_id, bank_num, preset_num); + + if(preset == NULL) + { + FLUID_LOG(FLUID_ERR, + "There is no preset with bank number %d and preset number %d in SoundFont %d", + bank_num, preset_num, sfont_id); + FLUID_API_RETURN(FLUID_FAILED); + } + + ret = fluid_preset_notify(preset, FLUID_PRESET_UNPIN, -1); // channel unused for pinning messages + + FLUID_API_RETURN(ret); +} + /** * Select an instrument on a MIDI channel by SoundFont name, bank and program numbers. * @param synth FluidSynth instance diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7981148c..fbc759a4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -11,6 +11,7 @@ ADD_FLUID_TEST(test_sample_cache) ADD_FLUID_TEST(test_sfont_loading) ADD_FLUID_TEST(test_sample_rate_change) ADD_FLUID_TEST(test_preset_sample_loading) +ADD_FLUID_TEST(test_preset_pinning) ADD_FLUID_TEST(test_bug_635) ADD_FLUID_TEST(test_pointer_alignment) ADD_FLUID_TEST(test_seqbind_unregister) diff --git a/test/test_preset_pinning.c b/test/test_preset_pinning.c new file mode 100644 index 00000000..0c155353 --- /dev/null +++ b/test/test_preset_pinning.c @@ -0,0 +1,194 @@ +#include "test.h" +#include "fluidsynth.h" +#include "sfloader/fluid_sfont.h" +#include "sfloader/fluid_defsfont.h" +#include "sfloader/fluid_samplecache.h" +#include "utils/fluid_sys.h" +#include "utils/fluid_list.h" + +static int count_loaded_samples(fluid_synth_t *synth, int sfont_id); + + +/* Test the preset pinning and unpinning feature */ +int main(void) +{ + int id; + fluid_synth_t *synth; + fluid_sfont_t *sfont; + fluid_defsfont_t *defsfont; + + /* Setup */ + fluid_settings_t *settings = new_fluid_settings(); + + + /* Test that pinning and unpinning has no effect and throws no errors + * when dynamic-sample-loading is disabled */ + fluid_settings_setint(settings, "synth.dynamic-sample-loading", 0); + synth = new_fluid_synth(settings); + id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 0); + TEST_ASSERT(count_loaded_samples(synth, id) == 123); + TEST_ASSERT(fluid_samplecache_count_entries() == 1); + + /* Attempt to pin and unpin an exising preset should succeed (but have no effect) */ + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 123); + + TEST_ASSERT(fluid_synth_unpin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 123); + + /* Attempt to pin and unpin a non-existant preset should fail */ + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 42, 42) == FLUID_FAILED); + TEST_ASSERT(count_loaded_samples(synth, id) == 123); + + TEST_ASSERT(fluid_synth_unpin_preset(synth, id, 42, 42) == FLUID_FAILED); + TEST_ASSERT(count_loaded_samples(synth, id) == 123); + + delete_fluid_synth(synth); + + + /* Test pinning and unpinning with dyamic-sample loading enabled */ + fluid_settings_setint(settings, "synth.dynamic-sample-loading", 1); + synth = new_fluid_synth(settings); + id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 0); + + TEST_ASSERT(count_loaded_samples(synth, id) == 0); + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + + /* For the following tests, preset 42 (Lead Synth 2) consists of 4 samples, + * preset 40 (Aluminum Plate) consists of 1 sample */ + + /* Simple pin and unpin */ + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_unpin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 0); + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + + /* Pinning and unpinning twice should have exactly the same effect */ + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_unpin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 0); + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + TEST_ASSERT(fluid_synth_unpin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 0); + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + + /* Pin a single preset, select both presets, unselect both, ensure the pinned + * is still in memory and gone after unpinning */ + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_program_select(synth, 0, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_program_select(synth, 1, id, 0, 40) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 5); + TEST_ASSERT(fluid_samplecache_count_entries() == 5); + + TEST_ASSERT(fluid_synth_unset_program(synth, 0) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 5); + TEST_ASSERT(fluid_samplecache_count_entries() == 5); + + TEST_ASSERT(fluid_synth_unset_program(synth, 1) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_unpin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 0); + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + + /* Unpinning an unpinned and selected preset should not remove it from memory */ + TEST_ASSERT(fluid_synth_program_select(synth, 0, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_unpin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_unset_program(synth, 0) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 0); + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + + /* Test unload of soundfont with pinned sample automatically unpins + * the sample and removes all samples from cache */ + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + TEST_ASSERT(fluid_synth_sfunload(synth, id, 0) == FLUID_OK); + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + + /* Ensure that failures during load of preset results in an + * error returned by pinning function */ + id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 0); + // hack the soundfont filename so the next load won't find the file anymore + sfont = fluid_synth_get_sfont_by_id(synth, id); + defsfont = fluid_sfont_get_data(sfont); + defsfont->filename[0]++; + + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_FAILED); + + defsfont->filename[0]--; + TEST_ASSERT(fluid_synth_sfunload(synth, id, 0) == FLUID_OK); + + + /* Test that deleting the synth with a pinned preset also leaves no + * samples in cache */ + id = fluid_synth_sfload(synth, TEST_SOUNDFONT, 0); + + TEST_ASSERT(fluid_synth_pin_preset(synth, id, 0, 42) == FLUID_OK); + TEST_ASSERT(count_loaded_samples(synth, id) == 4); + TEST_ASSERT(fluid_samplecache_count_entries() == 4); + + delete_fluid_synth(synth); + + TEST_ASSERT(fluid_samplecache_count_entries() == 0); + + + /* Tear down */ + delete_fluid_settings(settings); + + return EXIT_SUCCESS; +} + + +static int count_loaded_samples(fluid_synth_t *synth, int sfont_id) +{ + fluid_list_t *list; + int count = 0; + + fluid_sfont_t *sfont = fluid_synth_get_sfont_by_id(synth, sfont_id); + fluid_defsfont_t *defsfont = fluid_sfont_get_data(sfont); + + for(list = defsfont->sample; list; list = fluid_list_next(list)) + { + fluid_sample_t *sample = fluid_list_get(list); + + if(sample->data != NULL) + { + count++; + } + } + + FLUID_LOG(FLUID_INFO, "Loaded samples on sfont %d: %d\n", sfont_id, count); + return count; +}