Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: SoundFont Loading Refactor #360

Merged
merged 47 commits into from Apr 7, 2018
Merged

RFC: SoundFont Loading Refactor #360

merged 47 commits into from Apr 7, 2018

Conversation

mawe42
Copy link
Member

@mawe42 mawe42 commented Apr 1, 2018

Important: This isn't a finished pull request, but a work in progress. So please do not merge this in it's current form.

I've started this large refactor because I wanted to add a new feature to the soundfont loader: lazy-loading of presets and samples. The idea is that in certain real-time performance situations, small delays in switching presets for loading the required samples are absolutely acceptable. So on loading a new soundfont and selecting a preset, only the absolute minimum of information is actually read from the file and loaded to RAM. This would greatly reduce loading time and save huge amounts of memory for larger soundfonts that contain many instruments and presets. Please note: this feature hasn't been implemented in this PR. I'm just explaining this here to give some background to some of the changes I've made to the loading infrastructure.

Just a quick summary of what has already been done:

  • more the low-level soundfont loading code (the code taken over from Smurf) into a separate file
  • cleanup the low-level code, bringing the style more in line with the rest of FluidSynth's codebase
  • refactor the low-level code to prepare it for selective loading of preset zones, instruments and samples
  • move the ogg vorbis compression to fluid_sfont
  • refactor the sample start/end and loopstart/loopend checking, making it possible to use the same logic
    and code for both compressed and uncompressed samples
  • move the samplecache code from defsfont to own file, move sample loading code to low-level code in fluid_sffile
  • refactor and cleanup the samplecache code

Next I am going to work in the preset loading functions. But before I carry on and add more hours and work, I would really like to get some feedback on the direction I'm taking. Do the changes I've made so far look good, or do you think I'm going into the wrong direction?

Also, I've got one specific question: currently, whenever FluidSynth core requests a preset from a defsfont loader, a new fluid_preset_t struct is allocated... every time. To me that seems totally pointless, because that fluid_preset_t struct only contains pointers to the accessor functions and uses the (singleton) fluid_defpreset_t to do any real work anyway. So I'm thinking of changing the defsfont loader to pre-create those fluid_preset_t structs and only return a pointer to them when asked. That would mean we could get rid of the preset_stack dance in defspreset.

FLUID_LOG(FLUID_DBG, "Sample '%s': invalid loop start '%d', setting to sample start '%d'",
sample->name, sample->loopstart, sample->start);
sample->loopstart = sample->start;
modified = TRUE;
Copy link
Member

@derselbst derselbst Apr 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and below) actually introduces a regression for #171. As the comment points out we should not modify this in case of (sample->loopstart < sample->start) || (sample->loopend < sample->start), at least for SF2 files. This handling would be probably ok for SF3, as it does not share a big sample buffer like SF2.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it introduces a regression. It's not quite what the comment says, but that's how it is implemented in master:

invalid_loopstart = (sam->loopstart < sam->start) || (sam->loopstart >= sam->loopend);

And when invalid_loopstart is set, loopstart is set to sample->start.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sry, my bad. I was convinced it sounded wrong during a first rehearsal. But you're right, it's fine.

@@ -24,6 +24,10 @@

#include "fluidsynth.h"

FLUIDSYNTH_API int fluid_sample_validate(fluid_sample_t *sample, unsigned int max_end);
FLUIDSYNTH_API int fluid_sample_sanitize_loop(fluid_sample_t *sample, unsigned int max_end);
FLUIDSYNTH_API int fluid_sample_decompress_vorbis(fluid_sample_t *sample);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you making these public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point. That's a mistake.

Copy link
Member

@derselbst derselbst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the changes I've made so far look good, or do you think I'm going into the wrong direction?

Overall this looks good to me. It splits the huge overloaded fluid_defsfont.c into more handy parts. It might be worth bringing the lazy-loading feature to the mailing list for discussion, but that's a different story.

Also, I've got one specific question: currently, whenever FluidSynth core requests a preset from a defsfont loader, a new fluid_preset_t struct is allocated... every time. To me that seems totally pointless, because that fluid_preset_t struct only contains pointers to the accessor functions and uses the (singleton) fluid_defpreset_t to do any real work anyway. So I'm thinking of changing the defsfont loader to pre-create those fluid_preset_t structs and only return a pointer to them when asked. That would mean we could get rid of the preset_stack dance in defspreset.

Yes, that TODO has been on the list for long. Your proposal sounds good to me. Not quite sure about the schedule of your works. Would it be #360 -> Re-Formatting -> Preset-Stack-Refactor -> Lazy-Loading ? I'm open to postpone the Reformatting even further, just let me know.

Some notes below.

entry = new_samplecache_entry(sf, sample_start, sample_count);
if (entry == NULL)
{
return FLUID_FAILED;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return without mutex unlock

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted, thanks.

entry->mlocked = (fluid_mlock(entry->sample_data, entry->sample_count * 2) == 0);
if (entry->sample_data24 != NULL)
{
entry->mlocked &= (fluid_mlock(entry->sample_data24, entry->sample_count) == 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose: The first mlock succeeds, the second one fails, so that entry->mlocked is false, causing the memory not to be unlocked in fluid_samplecache_unload().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Will change it so that it only tries to the mlock 24-bit data if the first mlock succeeds.

{
#if defined(WIN32) || defined(__OS2__)
*modification_time = 0;
return FLUID_OK;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// FIXME: use g_file_info_get_modification_time()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a quickly smashed together C99 version that should retrieve the mod time of a file. Uncompiled and untested, as I'm currently not at home.

int res = FLUID_FAILED;

#if GLIB_CHECK_VERSION(2,26,0) // g_date_time_new_from_timeval_utc() requires at least 2.26
GFile* gfile = g_file_new_for_path (filename);

if(gfile != NULL)
{
GFileInfo* info = g_file_query_info (gfile,
                                  G_FILE_ATTRIBUTE_STANDARD_SIZE "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
                                  G_FILE_QUERY_INFO_NONE,
                                  NULL,
                                  NULL);
if (info != NULL)
{
                GTimeVal tv;
                g_file_info_get_modification_time (info, &tv);
                GDateTime* dt = g_date_time_new_from_timeval_utc(&tv);
                if(dt != NULL)
                {
                   // TODO: converting int64 to time_t, does it compile? is it safe??
                   *modification_time = g_date_time_to_unix(dt);
                    g_date_time_unref(dt);
                    res = FLUID_OK;
                }
                g_object_unref(info);
}
g_object_unref(gfile);
}
#else
#warning "Consider upgrading glib to at least version 2.26"
#endif
return res;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks! Regarding the g_date_time_new_from_timeval_utc: we could switch to using a uint64 for the modification_time in fluid_samplecache_t. Then we could just to the following:

*modification_time = tv.tv_sec * 1000000 + tv.tv_usec;

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sure. But unless you really need microsecond precision for a file mod date, *modification_time = tv.tv_sec should be sufficient (which should result in quite the same behaviour as g_date_time_to_unix()).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, microsecond precision is probably not required. So lets go with seconds and see how it goes. Precision can always be easily increased if it poses a problem...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the functions you used here are from GIO, not GLIB. I don't think adding another dependency for the modification time is worth it. But we could simply use g_stat:

--- fluid_sys.h:
#include <glib/gstdio.h>
typedef GStatBuf fluid_stat_buf_t;
#define fluid_stat(_filename, _statbuf)   g_stat((_filename), (_statbuf))

--- fluid_samplecache.c:
static int fluid_get_file_modification_time(char *filename, time_t *modification_time)
{
    fluid_stat_buf_t buf;

    if (fluid_stat(filename, &buf))
    {
        return FLUID_FAILED;
    }

    *modification_time = buf.st_mtime;
    return FLUID_OK;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didnt notice I was digging in GIO. You're right, and g_stat is the much more elegant way. Thx!

/* Load 16-bit sample data */
if (sf->fcbs->fseek(sf->sffd, sf->samplepos + (start * 2), SEEK_SET) == FLUID_FAILED)
{
perror("error");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perror seems redundant

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

goto error_exit;
}

loaded_data = (short *)FLUID_MALLOC(count * 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a friend of casting malloc() to the destination pointer type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll use FLUID_ARRAY.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... seeing that FLUID_ARRAY does exactly that (just hidden behind a macro)... what exactly do you propose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, now I understand. You just want to remove the explicit cast?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just want to remove the explicit cast?

Yes. But FLUID_ARRAY really seems to be the better solution for this purpose (just forgot about it).

seeing that FLUID_ARRAY does exactly that (just hidden behind a macro)...

At least the cast is "abstracted" through the macro, so I dont further care.

goto error_exit;
}

if (sf->sample24pos && ((start > sf->sample24size) || (end > sf->sample24size)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would postpone this check to the if (sf->sample24pos){...} below. And instead of hard failing in case there is an error related to SM24, I would prefer to just ignore the SM24 then instead of fatally failing to load the whole sample.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea. And more in line with how we treat 24-bit samples everywhere else.

if (loaded_data24 == NULL)
{
FLUID_LOG(FLUID_ERR, "Out of memory");
return FLUID_FAILED;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

goto error_exit; instead of direct return to avoid memory leak of loaded_data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack


struct _SFInst
{ /* Instrument structure */
unsigned short idx;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps better use unsigned int for this index?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... no idea why I chose short here...

@@ -170,6 +174,7 @@ struct _fluid_preset_t {
*/
struct _fluid_sample_t
{
unsigned short idx;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint idx; ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

@mawe42
Copy link
Member Author

mawe42 commented Apr 2, 2018

Overall this looks good to me. It splits the huge overloaded fluid_defsfont.c into more handy parts.

Great, thanks for your feedback and the initial review! I'll carry on then :-) There are a few more refactorings I'd like to do to the remaining defsfont code before attempting to add the new feature.

It might be worth bringing the lazy-loading feature to the mailing list for discussion, but that's a different story.

Ok, I will do that.

Not quite sure about the schedule of your works. Would it be #360 -> Re-Formatting -> Preset-Stack-Refactor -> Lazy-Loading ? I'm open to postpone the Reformatting even further, just let me know.

It would be great if we could postpone the reformatting until after the additional refactorings I'd like to do. So #360 -> Preset-Stack + a few other refactorings -> Re-Formatting -> Lazy Loading.

@mawe42
Copy link
Member Author

mawe42 commented Apr 3, 2018

I like your idea of only lazy loading sample data first. So I'm going to rebase this branch again, trying to remove most of the functional changes I made to the low-level loading in fluid_sffile that were in preparation for the lazy-loading of structural information. Should have kept this as a clean refactor in the first place and not mix it with functional changes... :-/

Then I will add the new modification time code and get rid of the preset stack and close this branch. Lazy loading will come in a new branch (and possibly after reformatting). Does that sound good to you, or would you rather have the preset stack stuff in a separate PR?

@derselbst
Copy link
Member

So I'm going to rebase this branch again, trying to remove most of the functional changes I made to the low-level loading in fluid_sffile

Ok. But generally I like the idea of having this fluid_sffile to handle the low-level loading.

If it's not too much trouble for you I would choose to have the preset stack separately. Apart from that this seems fine to me.

mawe42 added 24 commits April 4, 2018 11:03
# Conflicts:
#	src/sfloader/fluid_sf2.c
# Conflicts:
#	src/sfloader/fluid_sf2.h
Makes the whole file easier to read and gives the implementation a little
more "object-oriented" feel.
They were only used in soundfont loading code, so are probably a remnant
from Smurf. The rest of the FluidSynth code doesn't use underscore
functions, so remove them here as well for consistency.

Also use single quotes in double quoted string, to remove the need for
escaping chars.
@mawe42
Copy link
Member Author

mawe42 commented Apr 4, 2018

Ok, I've taken out all functional changes to the fluid_sffile code and rebased the branch. Apart from any bugfixes, I would call this refactor finished. The preset_stack removal will come in a new PR. And I don't really mind if you want to do the reformat before or after.

@derselbst
Copy link
Member

Ok will have a look at it. And just forget about the reformat at least for now, currently too much activity. Will see when it fits.

@mawe42
Copy link
Member Author

mawe42 commented Apr 4, 2018

Ah, g_stat requires at least glib 2.6. Will add a check and warning, just like in your suggested implementation.

@derselbst
Copy link
Member

No, fluidsynth requires 2.6.5 anyway:

pkg_check_modules ( GLIB REQUIRED glib-2.0>=2.6.5 gthread-2.0>=2.6.5 )

@mawe42
Copy link
Member Author

mawe42 commented Apr 4, 2018

No, fluidsynth requires 2.6.5 anyway

Perfect.

}
}

fluid_list_remove(samplecache_list, entry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be samplecache_list = fluid_list_remove(samplecache_list, entry);. Otherwise samplecache_list is not set to NULL when the last element is removed resulting in a NULL deref in line 248.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, well spotted.

return FLUID_FAILED;
}

return TRUE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because you're using FLUID_FAILED, this should be FLUID_OK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

@derselbst
Copy link
Member

So far testing worked as expected, no problems nor crashes. I also think it's time to add some unittest(s). I added one for the sample cache (#361), mainly aiming to test the ref count mechanism, i.e. to avoid using sample data from a soundfont that has already been unloaded by its parent synth.

There is a struct _fluid_sfont_info_t in fluid_synth.h which seems to be a bit redundant as it adds another indirection layer. I think it's members could just be integrated in struct _fluid_sfont_t as it's not a public struct anymore. And that fluid_synth_t::sfont_hash seems to be completely redundant? (just by passing the sfont_info directly

- fluid_synth_sfont_unref (synth, sfont_info->sfont);
+ fluid_synth_sfont_unref (synth, sfont_info);

in fluid_synth.c???)

Long story short: This PR is fine. If you want you can take care of those two minor issues right here or I'll have a try.

@mawe42
Copy link
Member Author

mawe42 commented Apr 6, 2018

Yes, that _fluid_sfont_info_t does look like it could be merged into fluid_sfont_t.

And the hash does look redundant. But I wonder what Element intended here... why implement this indirection? Maybe he didn't want to change fluid_sfont_t because it was public. And the hashtable to avoid having to iterate a list when searching for the fluid_sfont_info_t for a sfont?

If you want to have a go a those two, I would be glad. I'll do the preset_stack removal first, then prepare a lazy loading branch, just to flesh out a few ideas.

So I'll merge this PR, ok?

Copy link
Member

@derselbst derselbst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

@derselbst
Copy link
Member

Maybe he didn't want to change fluid_sfont_t because it was public. And the hashtable to avoid having to iterate a list when searching for the fluid_sfont_info_t for a sfont?

Just my thought.

@derselbst derselbst added this to the 2.0 milestone Apr 7, 2018
@mawe42 mawe42 merged commit 32961c4 into master Apr 7, 2018
@mawe42 mawe42 deleted the sfont-loader-refactor branch April 7, 2018 10:05
@derselbst
Copy link
Member

Just found that the unit test segfaulted with the vintagedreams sfont using your changes. Fixed a misplaced return FLUID_OK and modified the sample import code to not exit early, when it sees a broken sample:

32961c4...6ab1c7f

@mawe42
Copy link
Member Author

mawe42 commented Apr 7, 2018

Ah, thanks for catching that!

@mawe42
Copy link
Member Author

mawe42 commented Apr 7, 2018

I've just removed the noise debug messages for samples with loopstart = loopend: fa0d103

@mawe42 mawe42 mentioned this pull request Apr 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants