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