Skip to content

Commit

Permalink
Support simple but heavily compressed custom .sfo files
Browse files Browse the repository at this point in the history
Only available when compiling TSF together with stb_vorbis
This also add a sfotool to write such files
  • Loading branch information
schellingb committed Sep 13, 2023
1 parent ad9b00f commit 1961b13
Show file tree
Hide file tree
Showing 7 changed files with 462 additions and 67 deletions.
15 changes: 15 additions & 0 deletions sfotool/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.ncb
*.opt
*.plg
*.aps
*.ipch
*.suo
*.user
*.sdf
*.opensdf
*.dsw
*-i686
*-x86_64
Debug
Release
.vs
2 changes: 2 additions & 0 deletions sfotool/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
all:
gcc main.c -o sfotool
29 changes: 29 additions & 0 deletions sfotool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SFOTool for TinySoundFont
A tool to create heavily compressed .SFO files from .SF2 SoundFont files.

## Purpose
SFO files are just regular SoundFont v2 files with the entire block of raw PCM samples replaced
with a single Ogg Vorbis compressed stream. Unlike .sf3 files, which can have every separate font
sample compressed individually, this will compress the entire sound data as if it were a single
sample. This results in much higher compression than processing samples individually but also
higher loss of quality.

## Usage Help
```sh
sfotool <SF2/SFO>: Show type of sample stream contained (PCM or OGG)
sfotool <SF2> <WAV>: Dump PCM sample stream to .WAV file
sfotool <SFO> <OGG>: Dump OGG sample stream to .OGG file
sfotool <SF2/SFO> <WAV> <SF2>: Write new .SF2 soundfont file using PCM sample stream from .WAV file
sfotool <SF2/SFO> <OGG> <SFO>: Write new .SFO soundfont file using OGG sample stream from .OGG file
```

## Making a .SFO file from a .SF2 file
1. Dump the PCM data of a .SF2 file to a .WAV file
`sfotool <SF2> <WAV>`
2. Compress the .WAV file to .OGG (i.e. with [Audacity](https://www.audacityteam.org/download/legacy-windows/))
Make sure to choose the desired compression quality level
3. Build the .SFO file from the .SF2 file and the new .OGG
`sfotool <SF2> <OGG> <SFO>`

# License
SFOTool is available under the [Unlicense](http://unlicense.org/) (public domain).
200 changes: 200 additions & 0 deletions sfotool/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//--------------------------------------------//
// SFOTool //
// License: Public Domain (www.unlicense.org) //
//--------------------------------------------//

#include <stdio.h>
#include <string.h>

typedef char sfo_fourcc[4];
#define SFO_FourCCEquals(a, b) (a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3])
struct sfo_riffchunk { sfo_fourcc id; unsigned int size; };
struct sfo_wavheader
{
char RIFF[4]; unsigned int ChunkSize; char WAVE[4], fmt[4]; unsigned int Subchunk1Size;
unsigned short AudioFormat,NumOfChan; unsigned int SamplesPerSec, bytesPerSec;
unsigned short blockAlign, bitsPerSample; char Subchunk2ID[4]; unsigned int Subchunk2Size;
};

static void sfo_copy(FILE* src, FILE* trg, unsigned int size)
{
unsigned int block;
unsigned char buf[512];
for (; size; size -= block)
{
block = (size > sizeof(buf) ? sizeof(buf) : size);
fread(buf, 1, block, src);
fwrite(buf, 1, block, trg);
}
}

static int sfo_riffchunk_read(struct sfo_riffchunk* parent, struct sfo_riffchunk* chunk, FILE* f)
{
int is_riff, is_list;
if (parent && sizeof(sfo_fourcc) + sizeof(unsigned int) > parent->size) return 0;
if (!fread(&chunk->id, sizeof(sfo_fourcc), 1, f) || *chunk->id <= ' ' || *chunk->id >= 'z') return 0;
if (!fread(&chunk->size, sizeof(unsigned int), 1, f)) return 0;
if (parent && sizeof(sfo_fourcc) + sizeof(unsigned int) + chunk->size > parent->size) return 0;
if (parent) parent->size -= sizeof(sfo_fourcc) + sizeof(unsigned int) + chunk->size;
is_riff = SFO_FourCCEquals(chunk->id, "RIFF"), is_list = SFO_FourCCEquals(chunk->id, "LIST");
if (is_riff && parent) return 0; /* not allowed */
if (!is_riff && !is_list) return 1; /* custom type without sub type */
if (!fread(&chunk->id, sizeof(sfo_fourcc), 1, f) || *chunk->id <= ' ' || *chunk->id >= 'z') return 0;
chunk->size -= sizeof(sfo_fourcc);
return 1;
}

int main(int argc, const char** argv)
{
const char* arg_sf_in = (argc > 1 ? argv[1] : NULL);
const char* arg_smpl = (argc > 2 ? argv[2] : NULL);
const char* arg_sf_out = (argc > 3 ? argv[3] : NULL);
char ext_sf_in = (arg_sf_in ? arg_sf_in [strlen(arg_sf_in)-1] | 0x20 : '\0');
char ext_smpl = (arg_smpl ? arg_smpl [strlen(arg_smpl)-1] | 0x20 : '\0');
char ext_sf_out = (arg_sf_out ? arg_sf_out[strlen(arg_sf_out)-1] | 0x20 : '\0');
struct sfo_riffchunk chunkHead, chunkList, chunk;
FILE* f_sf_in = NULL, *f_smpl = NULL, *f_sf_out = NULL;

if (argc < 2 || argc > 4)
{
print_usage:
fprintf(stderr, "Usage Help:\n");
fprintf(stderr, "%s <SF2/SFO>: Show type of sample stream contained (PCM or OGG)\n", argv[0]);
fprintf(stderr, "%s <SF2> <WAV>: Dump PCM sample stream to .WAV file\n", argv[0]);
fprintf(stderr, "%s <SFO> <OGG>: Dump OGG sample stream to .OGG file\n", argv[0]);
fprintf(stderr, "%s <SF2/SFO> <WAV> <SF2>: Write new .SF2 soundfont file using PCM sample stream from .WAV file\n", argv[0]);
fprintf(stderr, "%s <SF2/SFO> <OGG> <SFO>: Write new .SFO soundfont file using OGG sample stream from .OGG file\n", argv[0]);
if (f_sf_in) fclose(f_sf_in);
if (f_smpl) fclose(f_smpl);
if (f_sf_out) fclose(f_sf_out);
return 1;
}

f_sf_in = fopen(arg_sf_in, "rb");
if (!f_sf_in) { fprintf(stderr, "Error: Passed input file '%s' does not exist\n\n", arg_sf_in); goto print_usage; }

if (!sfo_riffchunk_read(NULL, &chunkHead, f_sf_in) || !SFO_FourCCEquals(chunkHead.id, "sfbk"))
{
fprintf(stderr, "Error: Passed input file '%s' is not a valid soundfont file\n\n", arg_sf_in);
goto print_usage;
}
while (sfo_riffchunk_read(&chunkHead, &chunkList, f_sf_in))
{
unsigned int pos_listsize = (unsigned int)ftell(f_sf_in) - 8;
if (!SFO_FourCCEquals(chunkList.id, "sdta"))
{
fseek(f_sf_in, chunkList.size, SEEK_CUR);
continue;
}
for (; sfo_riffchunk_read(&chunkList, &chunk, f_sf_in); fseek(f_sf_in, chunkList.size, SEEK_CUR))
{
int is_pcm = SFO_FourCCEquals(chunk.id, "smpl");
if (!is_pcm && !SFO_FourCCEquals(chunk.id, "smpo"))
continue;

printf("Soundfont file '%s' contains a %s sample stream\n", arg_sf_in, (is_pcm ? "PCM" : "OGG"));
if (ext_sf_in != '2' && ext_sf_in != 'o') printf(" Warning: Soundfont file has unknown file extension (should be .SF2 or .SFO)\n");
if (ext_sf_in == '2' && !is_pcm) printf(" Warning: Soundfont file has .SF%c extension but sample stream is %s\n", '2', "OGG (should be .SFO)");
if (ext_sf_in == 'o' && is_pcm) printf(" Warning: Soundfont file has .SF%c extension but sample stream is %s\n", 'O', "PCM (should be .SF2)");
if (arg_sf_out)
{
unsigned int pos_smpchunk, end_smpchunk, len_smpl, end_sf, len_list_in, len_list_out;

printf("Writing file '%s' with samples from '%s'\n", arg_sf_out, arg_smpl);
if (ext_sf_out != '2' && ext_sf_out != 'o') printf(" Warning: Soundfont file has unknown file extension (should be .SF2 or .SFO)\n");
if (ext_smpl != 'v' && ext_smpl != 'g') printf(" Warning: Sample file has unknown file extension (should be .WAV or .OGG)\n");
if (ext_sf_out == '2' && ext_smpl != 'v') printf(" Warning: Soundfont file has .SF%c extension but sample file is .%s\n", '2', "OGG");
if (ext_sf_out == 'o' && ext_smpl == 'v') printf(" Warning: Soundfont file has .SF%c extension but sample file is .%s\n", 'O', "WAV");

f_smpl = fopen(arg_smpl, "rb");
if (!f_smpl) { fprintf(stderr, "Error: Unable to open input file '%s'\n\n", arg_smpl); goto print_usage; }

if (ext_smpl == 'v')
{
struct sfo_wavheader wav_hdr;
fread(&wav_hdr, sizeof(wav_hdr), 1, f_smpl);
if (!SFO_FourCCEquals(wav_hdr.Subchunk2ID, "data") || !SFO_FourCCEquals(wav_hdr.RIFF, "RIFF")
|| !SFO_FourCCEquals(wav_hdr.WAVE, "WAVE") || !SFO_FourCCEquals(wav_hdr.fmt, "fmt ")
|| wav_hdr.Subchunk1Size != 16 || wav_hdr.AudioFormat != 1 || wav_hdr.NumOfChan != 1
|| wav_hdr.bytesPerSec != wav_hdr.SamplesPerSec * sizeof(short) || wav_hdr.bitsPerSample != sizeof(short) * 8)
{ fprintf(stderr, "Input .WAV file is not a valid raw PCM encoded wave file\n\n"); goto print_usage; }

len_smpl = wav_hdr.Subchunk2Size;
}
else
{
fseek(f_smpl, 0, SEEK_END);
len_smpl = (unsigned int)ftell(f_smpl);
fseek(f_smpl, 0, SEEK_SET);
}

f_sf_out = fopen(arg_sf_out, "wb");
if (!f_sf_out) { fprintf(stderr, "Error: Unable to open output file '%s'\n\n", arg_sf_out); goto print_usage; }

pos_smpchunk = (unsigned int)(ftell(f_sf_in) - sizeof(struct sfo_riffchunk));
end_smpchunk = pos_smpchunk + (unsigned int)sizeof(struct sfo_riffchunk) + chunk.size;
fseek(f_sf_in, 0, SEEK_END);
end_sf = (unsigned int)ftell(f_sf_in);

/* Write data before list chunk size */
fseek(f_sf_in, 0, SEEK_SET);
sfo_copy(f_sf_in, f_sf_out, pos_listsize);

/* Write new list chunk size */
fread(&len_list_in, 4, 1, f_sf_in);
len_list_out = len_list_in - chunk.size + len_smpl;
fwrite(&len_list_out, 4, 1, f_sf_out);

/* Write data until sample chunk */
sfo_copy(f_sf_in, f_sf_out, pos_smpchunk - pos_listsize - 4);

/* Write sample chunk */
fwrite((ext_smpl == 'v' ? "smpl" : "smpo"), 4, 1, f_sf_out);
fwrite(&len_smpl, 4, 1, f_sf_out);
sfo_copy(f_smpl, f_sf_out, len_smpl);
fclose(f_smpl);

/* Write data after sample chunk */
fseek(f_sf_in, end_smpchunk, SEEK_SET);
sfo_copy(f_sf_in, f_sf_out, end_sf - end_smpchunk);
fclose(f_sf_out);
}
else if (arg_smpl)
{
f_smpl = fopen(arg_smpl, "wb");
printf("Writing file '%s' with %s sample stream\n", arg_smpl, (is_pcm ? "PCM" : "OGG"));
if (ext_smpl != 'v' && ext_smpl != 'g') printf(" Warning: Sample file has unknown file extension (should be .WAV or .OGG)\n");
if (ext_smpl == 'v' && !is_pcm) printf(" Warning: Sample file has .%s extension but sample stream is %s\n", "WAV", "OGG (should be .OGG)");
if (ext_smpl == 'g' && is_pcm) printf(" Warning: Sample file has .%s extension but sample stream is %s\n", "OGG", "PCM (should be .WAV)");
if (!f_smpl) { fprintf(stderr, "Unable to open output file '%s'\n\n", arg_smpl); goto print_usage; }

if (is_pcm)
{
struct sfo_wavheader wav_hdr;
memcpy(wav_hdr.Subchunk2ID, "data", 4);
memcpy(wav_hdr.RIFF, "RIFF", 4);
memcpy(wav_hdr.WAVE, "WAVE", 4);
memcpy(wav_hdr.fmt, "fmt ", 4);
wav_hdr.Subchunk1Size = 16;
wav_hdr.AudioFormat = 1;
wav_hdr.NumOfChan = 1;
wav_hdr.Subchunk2Size = (unsigned int)chunk.size;
wav_hdr.ChunkSize = sizeof(wav_hdr) - 4 - 4 + wav_hdr.Subchunk2Size;
wav_hdr.SamplesPerSec = 22050;
wav_hdr.bytesPerSec = 22050 * sizeof(short);
wav_hdr.blockAlign = 1 * sizeof(short);
wav_hdr.bitsPerSample = sizeof(short) * 8;
fwrite(&wav_hdr, sizeof(wav_hdr), 1, f_smpl);
}
sfo_copy(f_sf_in, f_smpl, chunk.size);
fclose(f_smpl);
printf("DONE\n");
}
fclose(f_sf_in);
return 0;
}
}

fprintf(stderr, "Passed input file is not a valid soundfont file\n\n");
goto print_usage;
}
28 changes: 28 additions & 0 deletions sfotool/sfotool.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.40629.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sfotool", "sfotool.vcxproj", "{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Win32 = Debug|Win32
Debug|x64 = Debug|x64
Release|Win32 = Release|Win32
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Debug|Win32.ActiveCfg = Debug|Win32
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Debug|Win32.Build.0 = Debug|Win32
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Debug|x64.ActiveCfg = Debug|x64
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Debug|x64.Build.0 = Debug|x64
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Release|Win32.ActiveCfg = Release|Win32
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Release|Win32.Build.0 = Release|Win32
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Release|x64.ActiveCfg = Release|x64
{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
100 changes: 100 additions & 0 deletions sfotool/sfotool.vcxproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{FFFFFFFF-FFFF-4FFF-FFFF-FFFFFFFFFFFF}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>sfotool</RootNamespace>
<ProjectName>sfotool</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '11.0' Or '$(PlatformToolsetVersion)' == '110' Or '$(MSBuildToolsVersion)' == '4.0'">v110_xp</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '12.0' Or '$(PlatformToolsetVersion)' == '120' Or '$(MSBuildToolsVersion)' == '12.0'">v120</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '14.0' Or '$(PlatformToolsetVersion)' == '140' Or '$(MSBuildToolsVersion)' == '14.0'">v140</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '15.0' Or '$(PlatformToolsetVersion)' == '141' Or '$(MSBuildToolsVersion)' == '15.0'">v141</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '16.0' Or '$(PlatformToolsetVersion)' == '142' Or '$(MSBuildToolsVersion)' == '16.0'">v142</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '17.0' Or '$(PlatformToolsetVersion)' == '143' Or '$(MSBuildToolsVersion)' == '17.0'">v143</PlatformToolset>
<PlatformToolset Condition="'$(PlatformToolset)' == ''">$(DefaultPlatformToolset)</PlatformToolset>
<UseOfMfc>false</UseOfMfc>
<CharacterSet>MultiByte</CharacterSet>
<OutDir>$(SolutionDir)$(Configuration)\$(ProjectName)_$(Platform)\</OutDir>
<IntDir>$(SolutionDir)$(Configuration)\$(ProjectName)_$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<IntrinsicFunctions>true</IntrinsicFunctions>
<ExceptionHandling>false</ExceptionHandling>
<BufferSecurityCheck>false</BufferSecurityCheck>
<RuntimeTypeInfo>false</RuntimeTypeInfo>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateMapFile>true</GenerateMapFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<AdditionalOptions Condition="'$(VisualStudioVersion)' &gt;= '12.0' Or '$(PlatformToolsetVersion)' &gt;= '120' Or '$(MSBuildToolsVersion)' &gt;= '12.0'">/Gw %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>false</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ItemGroup>
<ClCompile Include="main.c" />
</ItemGroup>
</Project>
Loading

0 comments on commit 1961b13

Please sign in to comment.