schellingb / TinySoundFont

SoundFont2 synthesizer library in a single C/C++ file
MIT License
623 stars 72 forks source link

Extending sfotool support with optional sample load callback. #84

Closed ghost closed 9 months ago

ghost commented 10 months ago

Just wondering: Is sfotool tied to exactly Vorbis?

I did some experiments for WTFweg's MIDI soundfont compression, using sfotool since the idea of "solid" sample compression (treating all compressible data as one contiguous stream), is quite nice.

I experimented with a 9MB SC55 soundfont.

Screenshot 2023-12-10 064045

FLAC is proving quite viable as a compression format for samples, and so is WavPack and Opus (especially with it being refined for lower bitrates, so less artifacts than Vorbis). TAK is even more viable for lossless compression but a 1st party closed source decoder library is required (if not regutting the decoder from FFmpeg).

So far rebuilding libopus/celt in my libretro based music player Detonate into its own simple library, sorta dr_flac/mp3/stb_vorbis style.

Would it be possible though for sample depacking to have an optional callback for loading SFO samples? That way anyone can use their own encoder/decoder libraries for samples?

schellingb commented 10 months ago

Hey there!

We could do something like this in tsf_load_samples:

#ifdef TSF_SAMPLE_LOAD_CALLBACK
    return TSF_SAMPLE_LOAD_CALLBACK(*pRawBuffer, chunkSmpl->size, pFloatBuffer, pSmplCount);
#else
    if (chunkSmpl->id[3] != 'o') return 1;

    // Decode custom .sfo 'smpo' format where all samples are in a single ogg stream
    resNum = resMax = 0;
    if (!tsf_decode_ogg((tsf_u8*)*pRawBuffer, (tsf_u8*)*pRawBuffer + chunkSmpl->size, pFloatBuffer, &resNum, &resMax, 65536)) return 0;
    if (!(*pFloatBuffer = (float*)TSF_REALLOC((oldres = *pFloatBuffer), resNum * sizeof(float)))) *pFloatBuffer = oldres;
    *pSmplCount = resNum;
    return (*pFloatBuffer ? 1 : 0);
#endif

Then the library can be used like:

int tsf_my_sample_loader(const void *raw_buffer, unsigned int raw_size, float** out_floats, unsigned int* out_count);
#define TSF_SAMPLE_LOAD_CALLBACK tsf_my_sample_loader
#define TSF_IMPLEMENTATION
#include "tsf.h"

Then the callback can do whatever it wants with the raw bytes in raw_buffer of size raw_size, allocate the memory for out_floats and write the number of samples it decoded into out_count.

An option could be to also pass chunkSmpl->id to the callback, so it could do something like sfotool does with replacing the fourcc "smpl" with "smpo". Though then we'd likely need another callback to customize the currently fixed behavior in tsf_load which only accepts "smpo" as an alternative fourcc to "smpl" when STB_VORBIS_INCLUDE_STB_VORBIS_H is defined. The custom callback perhaps might want to accept more than just "smpl" or "smpo".

ghost commented 10 months ago

The first option should be fine. As long as the sample decoder can read a buffer, and output some floats, it should be fine.