From 073da2141c6e5e7d6bd6a2ae7136c6b4fdf51298 Mon Sep 17 00:00:00 2001 From: Marcus Weseloh Date: Sun, 15 Apr 2018 22:39:27 +0200 Subject: [PATCH] Implement dynamic sample loading This change adds a new feature that enables loading and unloading of sample data on demand. As soon as a preset is selected for a channel, all of it's samples are loaded into memory. When a preset is unselected, all of it's samples are unloaded from memory (unless they are being used by another selected preset). This feature is disabled by default and can be switched on with the "synth.dynamic-sample-loading" setting. --- doc/fluidsettings.xml | 9 ++ src/sfloader/fluid_defsfont.c | 204 ++++++++++++++++++++++++++++++++-- src/sfloader/fluid_defsfont.h | 3 +- src/sfloader/fluid_sfont.h | 13 +++ src/synth/fluid_synth.c | 2 + 5 files changed, 219 insertions(+), 12 deletions(-) diff --git a/doc/fluidsettings.xml b/doc/fluidsettings.xml index 5b15570c..88dfda6d 100644 --- a/doc/fluidsettings.xml +++ b/doc/fluidsettings.xml @@ -66,6 +66,15 @@ https://stackoverflow.com/a/6251757 Device identifier used for SYSEX commands, such as MIDI Tuning Standard commands. Only those SYSEX commands destined for this ID or to all devices will be acted upon. + + dynamic-sample-loading + bool + 0 (TRUE) + + When set to 1 (TRUE), samples are loaded to and unloaded from memory + on demand. + + effects-channels int diff --git a/src/sfloader/fluid_defsfont.c b/src/sfloader/fluid_defsfont.c index 980b37d5..5f6b3c85 100644 --- a/src/sfloader/fluid_defsfont.c +++ b/src/sfloader/fluid_defsfont.c @@ -33,6 +33,13 @@ * compatible as most existing soundfonts expect exactly this (strange, non-standard) behaviour. */ #define EMU_ATTENUATION_FACTOR (0.4f) +/* Dynamic sample loading functions */ +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); +static int dynamic_samples_preset_notify(fluid_preset_t *preset, int reason, int chan); +static int dynamic_samples_sample_notify(fluid_sample_t *sample, int reason); + /*************************************************************** * @@ -195,6 +202,7 @@ fluid_defsfont_t* new_fluid_defsfont(fluid_settings_t* settings) FLUID_MEMSET(defsfont, 0, sizeof(*defsfont)); fluid_settings_getint(settings, "synth.lock-memory", &defsfont->mlock); + fluid_settings_getint(settings, "synth.dynamic-sample-loading", &defsfont->dynamic_samples); return defsfont; } @@ -260,7 +268,7 @@ int fluid_defsfont_load_sampledata(fluid_defsfont_t *defsfont, SFData *sfdata, f int num_samples; num_samples = fluid_samplecache_load( - sfdata, sample->start, sample->end, sample->sampletype, + sfdata, sample->source_start, sample->source_end, sample->sampletype, defsfont->mlock, &sample->data, &sample->data24); if (num_samples < 0) @@ -279,8 +287,8 @@ int fluid_defsfont_load_sampledata(fluid_defsfont_t *defsfont, SFData *sfdata, f * but SF2 samples are relative to sample chunk start, so they need to be adjusted */ if (!(sample->sampletype & FLUID_SAMPLETYPE_OGG_VORBIS)) { - sample->loopstart = sample->loopstart - sample->start; - sample->loopend = sample->loopend - sample->start; + sample->loopstart = sample->source_loopstart - sample->source_start; + sample->loopend = sample->source_loopend - sample->source_start; } /* As we've just loaded an individual sample into it's own buffer, we need to adjust the start @@ -364,6 +372,8 @@ int fluid_defsfont_load(fluid_defsfont_t* defsfont, const fluid_file_callbacks_t return FLUID_FAILED; } + defsfont->fcbs = fcbs; + /* The actual loading is done in the sfont and sffile files */ sfdata = fluid_sffile_open(file, fcbs); if (sfdata == NULL) { @@ -406,11 +416,14 @@ int fluid_defsfont_load(fluid_defsfont_t* defsfont, const fluid_file_callbacks_t p = fluid_list_next(p); } - /* Load all sample data */ - if (fluid_defsfont_load_all_sampledata(defsfont, sfdata) == FLUID_FAILED) + /* If dynamic sample loading is disabled, load all samples in the Soundfont */ + if (!defsfont->dynamic_samples) { - FLUID_LOG(FLUID_ERR, "Unable to load all sample data"); - goto err_exit; + if (fluid_defsfont_load_all_sampledata(defsfont, sfdata) == FLUID_FAILED) + { + FLUID_LOG(FLUID_ERR, "Unable to load all sample data"); + goto err_exit; + } } /* Load all the presets */ @@ -464,6 +477,11 @@ int fluid_defsfont_add_preset(fluid_defsfont_t* defsfont, fluid_defpreset_t* def fluid_defpreset_preset_noteon, fluid_defpreset_preset_delete); + if (defsfont->dynamic_samples) + { + preset->notify = dynamic_samples_preset_notify; + } + if (preset == NULL) { return FLUID_FAILED; } @@ -1600,15 +1618,26 @@ int fluid_sample_import_sfont(fluid_sample_t* sample, SFSample* sfsample, fluid_defsfont_t* defsfont) { FLUID_STRCPY(sample->name, sfsample->name); - sample->start = sfsample->start; - sample->end = (sfsample->end > 0) ? sfsample->end - 1 : 0; /* marks last sample, contrary to SF spec. */ - sample->loopstart = sfsample->loopstart; - sample->loopend = sfsample->loopend; + + sample->source_start = sfsample->start; + sample->source_end = (sfsample->end > 0) ? sfsample->end - 1 : 0; /* marks last sample, contrary to SF spec. */ + sample->source_loopstart = sfsample->loopstart; + sample->source_loopend = sfsample->loopend; + + sample->start = sample->source_start; + sample->end = sample->source_end; + sample->loopstart = sample->source_loopstart; + sample->loopend = sample->source_loopend; sample->samplerate = sfsample->samplerate; sample->origpitch = sfsample->origpitch; sample->pitchadj = sfsample->pitchadj; sample->sampletype = sfsample->sampletype; + if (defsfont->dynamic_samples) + { + sample->notify = dynamic_samples_sample_notify; + } + if (fluid_sample_validate(sample, defsfont->samplesize) == FLUID_FAILED) { return FLUID_FAILED; @@ -1616,3 +1645,156 @@ fluid_sample_import_sfont(fluid_sample_t* sample, SFSample* sfsample, fluid_defs return FLUID_OK; } + +/* Called if a sample is no longer used by a voice. Used by dynamic sample loading + * to unload a sample that is not used by any loaded presets anymore but couldn't + * be unloaded straight away because it was still in use by a voice. */ +static int dynamic_samples_sample_notify(fluid_sample_t *sample, int reason) +{ + if (reason == FLUID_SAMPLE_DONE && sample->preset_count == 0) + { + unload_sample(sample); + } + + return FLUID_OK; +} + +/* Called if a preset has been selected for or unselected from a channel. Used by + * dynamic sample loading to load and unload samples on demand. */ +static int dynamic_samples_preset_notify(fluid_preset_t *preset, int reason, int chan) +{ + fluid_defsfont_t *defsfont; + + if (reason == FLUID_PRESET_SELECTED) + { + 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); + } + else 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 FLUID_OK; +} + + +/* Walk through all samples used by the passed in preset and make sure that the + * sample data is loaded for each sample. Used by dynamic sample loading. */ +static int load_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset) +{ + fluid_defpreset_t *defpreset; + fluid_preset_zone_t *preset_zone; + fluid_inst_t *inst; + fluid_inst_zone_t *inst_zone; + fluid_sample_t *sample; + SFData *sffile; + + sffile = fluid_sffile_open(defsfont->filename, defsfont->fcbs); + if (sffile == NULL) + { + FLUID_LOG(FLUID_ERR, "Unable to open Soundfont file"); + return FLUID_FAILED; + } + + defpreset = fluid_preset_get_data(preset); + preset_zone = fluid_defpreset_get_zone(defpreset); + while (preset_zone != NULL) + { + inst = fluid_preset_zone_get_inst(preset_zone); + inst_zone = fluid_inst_get_zone(inst); + while (inst_zone != NULL) { + sample = fluid_inst_zone_get_sample(inst_zone); + + if ((sample != NULL) && (sample->start != sample->end)) + { + sample->preset_count++; + + if (sample->preset_count == 1) + { + /* This is the first time this sample has been selected, so + * load the sampledata */ + if (fluid_defsfont_load_sampledata(defsfont, sffile, sample) == FLUID_FAILED) + { + FLUID_LOG(FLUID_ERR, "Unable to load sample '%s', disabling", sample->name); + sample->start = sample->end = 0; + } + } + } + + inst_zone = fluid_inst_zone_next(inst_zone); + } + preset_zone = fluid_preset_zone_next(preset_zone); + } + + fluid_sffile_close(sffile); + + return FLUID_OK; +} + +/* Walk through all samples used by the passed in preset and unload the sample data + * of each sample that is not used by any selected preset anymore. Used by dynamic + * sample loading. */ +static int unload_preset_samples(fluid_defsfont_t *defsfont, fluid_preset_t *preset) +{ + fluid_defpreset_t *defpreset; + fluid_preset_zone_t *preset_zone; + fluid_inst_t *inst; + fluid_inst_zone_t *inst_zone; + fluid_sample_t *sample; + + defpreset = fluid_preset_get_data(preset); + preset_zone = fluid_defpreset_get_zone(defpreset); + while (preset_zone != NULL) + { + inst = fluid_preset_zone_get_inst(preset_zone); + inst_zone = fluid_inst_get_zone(inst); + while (inst_zone != NULL) { + sample = fluid_inst_zone_get_sample(inst_zone); + + if ((sample != NULL) && (sample->preset_count > 0)) + { + sample->preset_count--; + + /* If the sample is not used by any preset or used by a + * sounding voice, unload it from the sample cache. If it's + * still in use by a voice, dynamic_samples_sample_notify will + * take care of unloading the sample as soon as the voice is + * finished with it (but only on the next API call). */ + if (sample->preset_count == 0 && sample->refcount == 0) + { + unload_sample(sample); + } + } + + inst_zone = fluid_inst_zone_next(inst_zone); + } + preset_zone = fluid_preset_zone_next(preset_zone); + } + + return FLUID_OK; +} + +/* Unload an unused sample from the samplecache */ +static void unload_sample(fluid_sample_t *sample) +{ + fluid_return_if_fail(sample != NULL); + fluid_return_if_fail(sample->data != NULL); + fluid_return_if_fail(sample->preset_count == 0); + fluid_return_if_fail(sample->refcount == 0); + + FLUID_LOG(FLUID_DBG, "Unloading sample '%s'", sample->name); + + if (fluid_samplecache_unload(sample->data) == FLUID_FAILED) + { + FLUID_LOG(FLUID_ERR, "Unable to unload sample '%s'", sample->name); + } + else + { + sample->data = NULL; + sample->data24 = NULL; + } +} diff --git a/src/sfloader/fluid_defsfont.h b/src/sfloader/fluid_defsfont.h index 9909e4b9..11755953 100644 --- a/src/sfloader/fluid_defsfont.h +++ b/src/sfloader/fluid_defsfont.h @@ -94,6 +94,7 @@ int fluid_zone_inside_range(fluid_zone_range_t* zone_range, int key, int vel); */ struct _fluid_defsfont_t { + const fluid_file_callbacks_t* fcbs; /* the file callbacks used to load this Soundfont */ char* filename; /* the filename of this soundfont */ unsigned int samplepos; /* the position in the file at which the sample data starts */ unsigned int samplesize; /* the size of the sample data in bytes */ @@ -107,6 +108,7 @@ struct _fluid_defsfont_t fluid_list_t* sample; /* the samples in this soundfont */ fluid_list_t* preset; /* the presets of this soundfont */ int mlock; /* Should we try memlock (avoid swapping)? */ + int dynamic_samples; /* Enables dynamic sample loading if set */ fluid_list_t *preset_iter_cur; /* the current preset in the iteration */ }; @@ -213,7 +215,6 @@ int fluid_inst_zone_import_sfont(fluid_preset_zone_t* preset_zone, fluid_sample_t* fluid_inst_zone_get_sample(fluid_inst_zone_t* zone); - int fluid_sample_import_sfont(fluid_sample_t* sample, SFSample* sfsample, fluid_defsfont_t* defsfont); int fluid_sample_in_rom(fluid_sample_t* sample); diff --git a/src/sfloader/fluid_sfont.h b/src/sfloader/fluid_sfont.h index d31ccfb0..9d3ee6e6 100644 --- a/src/sfloader/fluid_sfont.h +++ b/src/sfloader/fluid_sfont.h @@ -176,10 +176,22 @@ struct _fluid_preset_t { struct _fluid_sample_t { char name[21]; /**< Sample name */ + + /* The following for sample pointers store the original pointers from the Soundfont + * file. They are never changed after loading and are used to re-create the + * actual sample pointers after a sample has been unloaded and loaded again. The + * actual sample pointers get modified during loading for SF3 (compressed) samples + * and individually loaded SF2 samples. */ + unsigned int source_start; + unsigned int source_end; + unsigned int source_loopstart; + unsigned int source_loopend; + unsigned int start; /**< Start index */ unsigned int end; /**< End index, index of last valid sample point (contrary to SF spec) */ unsigned int loopstart; /**< Loop start index */ unsigned int loopend; /**< Loop end index, first point following the loop (superimposed on loopstart) */ + unsigned int samplerate; /**< Sample rate */ int origpitch; /**< Original pitch (MIDI note number, 0-127) */ int pitchadj; /**< Fine pitch adjustment (+/- 99 cents) */ @@ -192,6 +204,7 @@ struct _fluid_sample_t double amplitude_that_reaches_noise_floor; /**< The amplitude at which the sample's loop will be below the noise floor. For voice off optimization, calculated automatically. */ unsigned int refcount; /**< Count of voices using this sample */ + int preset_count; /**< Count of selected presets using this sample (used for dynamic sample loading) */ /** * Implement this function to receive notification when sample is no longer used. diff --git a/src/synth/fluid_synth.c b/src/synth/fluid_synth.c index 44e3ecfa..3c10a760 100644 --- a/src/synth/fluid_synth.c +++ b/src/synth/fluid_synth.c @@ -228,6 +228,8 @@ void fluid_synth_settings(fluid_settings_t* settings) fluid_settings_add_option(settings, "synth.midi-bank-select", "gs"); fluid_settings_add_option(settings, "synth.midi-bank-select", "xg"); fluid_settings_add_option(settings, "synth.midi-bank-select", "mma"); + + fluid_settings_register_int(settings, "synth.dynamic-sample-loading", 0, 0, 1, FLUID_HINT_TOGGLED); } /**