Add public API to pin and unpin presets to the sample cache (#698)

Following the discussion about an API to pin and unpin preset samples in the sample cache here:
https://lists.nongnu.org/archive/html/fluid-dev/2020-10/msg00016.html

Short explanation of the change:

Only the default loader currently supports dynamic sample loading, so I thought it might be a good idea to keep the changes for this feature mostly contained in the default loader as well. I've added two new preset notify flags (FLUID_PRESET_PIN and FLUID_PRESET_UNPIN) that are handled by the preset->notify callback and trigger the loading and possibly unloading of the samples.
This commit is contained in:
Marcus Weseloh 2020-10-31 13:23:15 +01:00 committed by GitHub
parent 58ccd6d53e
commit 109c41c355
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 399 additions and 5 deletions

View file

@ -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?

View file

@ -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 */
};
/**

View file

@ -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()
*/

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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 */

View file

@ -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++; }

View file

@ -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 <code>synth.dynamic-sample-loading</code> 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 <code>synth.dynamic-sample-loading</code> 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 <code>synth.dynamic-sample-loading</code> 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

View file

@ -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)

194
test/test_preset_pinning.c Normal file
View file

@ -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;
}