shithub: sf2mid

Download patch

ref: 1961b134aff2e1226c070d2b006b889b7e80dc5d
parent: ad9b00f65a7810370908ed56e188b5a4ed61e14e
author: Bernhard Schelling <14200249+schellingb@users.noreply.github.com>
date: Wed Sep 13 21:36:04 EDT 2023

Support simple but heavily compressed custom .sfo files
Only available when compiling TSF together with stb_vorbis
This also add a sfotool to write such files

diff: cannot open b/sfotool//null: file does not exist: 'b/sfotool//null'
--- /dev/null
+++ b/sfotool/.gitignore
@@ -1,0 +1,15 @@
+*.ncb
+*.opt
+*.plg
+*.aps
+*.ipch
+*.suo
+*.user
+*.sdf
+*.opensdf
+*.dsw
+*-i686
+*-x86_64
+Debug
+Release
+.vs
--- /dev/null
+++ b/sfotool/Makefile
@@ -1,0 +1,2 @@
+all:
+	gcc main.c -o sfotool
--- /dev/null
+++ b/sfotool/README.md
@@ -1,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).
--- /dev/null
+++ b/sfotool/main.c
@@ -1,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;
+}
--- /dev/null
+++ b/sfotool/sfotool.sln
@@ -1,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
--- /dev/null
+++ b/sfotool/sfotool.vcxproj
@@ -1,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>
\ No newline at end of file
--- a/tsf.h
+++ b/tsf.h
@@ -21,7 +21,7 @@
 
    LICENSE (MIT)
 
-   Copyright (C) 2017, 2018 Bernhard Schelling
+   Copyright (C) 2017-2023 Bernhard Schelling
    Based on SFZero, Copyright (C) 2012 Steve Folta (https://github.com/stevefolta/SFZero)
 
    Permission is hereby granted, free of charge, to any person obtaining a copy of this
@@ -862,11 +862,53 @@
 	return 1;
 }
 
-static int tsf_decode_samples(tsf_u8* smplBuffer, tsf_u32 smplLength, float** outSamples, unsigned int* outSampleCount, struct tsf_hydra *hydra)
+#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
+static TSF_BOOL tsf_decode_ogg(const tsf_u8 *pSmpl, const tsf_u8 *pSmplEnd, float** pRes, tsf_u32* pResNum, tsf_u32* pResMax, tsf_u32 resInitial)
 {
-	#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
+	float* res = *pRes; tsf_u32 resNum = *pResNum; tsf_u32 resMax = *pResMax; stb_vorbis *v;
+
+	// Use whatever stb_vorbis API that is available (either pull or push)
+	#if !defined(STB_VORBIS_NO_PULLDATA_API) && !defined(STB_VORBIS_NO_FROMMEMORY)
+	v = stb_vorbis_open_memory(pSmpl, (int)(pSmplEnd - pSmpl), TSF_NULL, TSF_NULL);
+	#else
+	{ int use, err; v = stb_vorbis_open_pushdata(pSmpl, (int)(pSmplEnd - pSmpl), &use, &err, TSF_NULL); pSmpl += use; }
+	#endif
+	if (v == TSF_NULL) return TSF_FALSE;
+
+	for (;;)
+	{
+		float** outputs; int n_samples;
+
+		// Decode one frame of vorbis samples with whatever stb_vorbis API that is available
+		#if !defined(STB_VORBIS_NO_PULLDATA_API) && !defined(STB_VORBIS_NO_FROMMEMORY)
+		n_samples = stb_vorbis_get_frame_float(v, TSF_NULL, &outputs);
+		if (!n_samples) break;
+		#else
+		if (pSmpl >= pSmplEnd) break;
+		{ int use = stb_vorbis_decode_frame_pushdata(v, pSmpl, (int)(pSmplEnd - pSmpl), TSF_NULL, &outputs, &n_samples); pSmpl += use; }
+		if (!n_samples) continue;
+		#endif
+
+		// Expand our output buffer if necessary then copy over the decoded frame samples
+		resNum += n_samples;
+		if (resNum > resMax)
+		{
+			do { resMax += (resMax ? (resMax < 1048576 ? resMax : 1048576) : resInitial); } while (resNum > resMax);
+			res = (float*)TSF_REALLOC(res, resMax * sizeof(float));
+			if (!res) { stb_vorbis_close(v); return 0; }
+		}
+		TSF_MEMCPY(res + resNum - n_samples, outputs[0], n_samples * sizeof(float));
+	}
+	stb_vorbis_close(v);
+	*pRes = res; *pResNum = resNum; *pResMax = resMax;
+	return TSF_TRUE;
+}
+
+static int tsf_decode_sf3_samples(const void* rawBuffer, float** pFloatBuffer, unsigned int* pSmplCount, struct tsf_hydra *hydra)
+{
+	const tsf_u8* smplBuffer = (const tsf_u8*)rawBuffer;
+	tsf_u32 smplLength = *pSmplCount, resNum = 0, resMax = 0, resInitial = (smplLength > 0x100000 ? (smplLength & ~0xFFFFF) : 65536);
 	float *res = TSF_NULL;
-	tsf_u32 resNum = 0, resMax = 0, resInitial = (smplLength > 0x100000 ? (smplLength & ~0xFFFFF) : 65536);
 	int i;
 	for (i = 0; i < hydra->shdrNum; i++)
 	{
@@ -874,7 +916,6 @@
 		if (shdr->end <= shdr->start) continue;
 		if (shdr->sampleType & 0x30) // compression flags (sometimes Vorbis flag)
 		{
-			stb_vorbis *v;
 			const tsf_u8 *pSmpl = smplBuffer + shdr->start, *pSmplEnd = smplBuffer + shdr->end;
 			if (!TSF_FourCCEquals(pSmpl, "OggS"))
 			{
@@ -882,44 +923,12 @@
 				continue;
 			}
 
-			// Use whatever stb_vorbis API that is available (either pull or push)
-			#if !defined(STB_VORBIS_NO_PULLDATA_API) && !defined(STB_VORBIS_NO_FROMMEMORY)
-			v = stb_vorbis_open_memory(pSmpl, (int)(pSmplEnd - pSmpl), TSF_NULL, TSF_NULL);
-			#else
-			{ int use, err; v = stb_vorbis_open_pushdata(pSmpl, (int)(pSmplEnd - pSmpl), &use, &err, TSF_NULL); pSmpl += use; }
-			#endif
-			if (v == TSF_NULL) { TSF_FREE(res); return 0; }
-
 			// Fix up sample indices in shdr (end index is set after decoding)
 			shdr->start = resNum;
 			shdr->startLoop += resNum;
 			shdr->endLoop += resNum;
-			for (;;)
-			{
-				float** outputs; int n_samples;
-
-				// Decode one frame of vorbis samples with whatever stb_vorbis API that is available
-				#if !defined(STB_VORBIS_NO_PULLDATA_API) && !defined(STB_VORBIS_NO_FROMMEMORY)
-				n_samples = stb_vorbis_get_frame_float(v, TSF_NULL, &outputs);
-				if (!n_samples) break;
-				#else
-				if (pSmpl >= pSmplEnd) break;
-				{ int use = stb_vorbis_decode_frame_pushdata(v, pSmpl, (int)(pSmplEnd - pSmpl), TSF_NULL, &outputs, &n_samples); pSmpl += use; }
-				if (!n_samples) continue;
-				#endif
-
-				// Expand our output buffer if necessary then copy over the decoded frame samples
-				resNum += n_samples;
-				if (resNum > resMax)
-				{
-					do { resMax += (resMax ? (resMax < 1048576 ? resMax : 1048576) : resInitial); } while (resNum > resMax);
-					res = (float*)TSF_REALLOC(res, resMax * sizeof(float));
-					if (!res) { stb_vorbis_close(v); return 0; }
-				}
-				TSF_MEMCPY(res + resNum - n_samples, outputs[0], n_samples * sizeof(float));
-			}
+			if (!tsf_decode_ogg(pSmpl, pSmplEnd, &res, &resNum, &resMax, resInitial)) { TSF_FREE(res); return 0; }
 			shdr->end = resNum;
-			stb_vorbis_close(v);
 		}
 		else // raw PCM sample
 		{
@@ -951,30 +960,37 @@
 
 	// Trim the sample buffer down then return success (unless out of memory)
 	res = (float*)TSF_REALLOC(res, resNum * sizeof(float));
-	*outSamples = res;
-	*outSampleCount = resNum;
+	*pFloatBuffer = res;
+	*pSmplCount = resNum;
 	return (res ? 1 : 0);
-	#else
-	// Inline convert the samples from short to float (buffer was allocated big enough in tsf_load_samples)
-	float *out; const short *in;
-	*outSamples = (float*)smplBuffer;
-	*outSampleCount = smplLength / sizeof(short);
-	for (in = (short*)smplBuffer + *outSampleCount, out = *outSamples + *outSampleCount; in != (short*)smplBuffer;)
-		*(--out) = (float)(*(--in) / 32767.0);
-	return 1;
-	#endif
 }
+#endif
 
-static int tsf_load_samples(tsf_u8** smplBuffer, tsf_u32 smplLength, struct tsf_stream* stream)
+static int tsf_load_samples(void** pRawBuffer, float** pFloatBuffer, unsigned int* pSmplCount, struct tsf_riffchunk *chunkSmpl, struct tsf_stream* stream)
 {
 	#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
-	// With OGG Vorbis support we cannot pre-allocate the memory for tsf_decode_samples
-	*smplBuffer = (tsf_u8*)TSF_MALLOC(smplLength);
+	// With OGG Vorbis support we cannot pre-allocate the memory for tsf_decode_sf3_samples
+	*pSmplCount = chunkSmpl->size;
+	*pRawBuffer = (void*)TSF_MALLOC(*pSmplCount);
+	if (!*pRawBuffer || !stream->read(stream->data, *pRawBuffer, chunkSmpl->size)) return 0;
+	if (chunkSmpl->id[3] != 'o') return 1;
+
+	// Decode custom .sfo 'smpo' format where all samples are in a single ogg stream
+	tsf_u32 resNum = 0, resMax = 0, resInitial = 65536;
+	if (!tsf_decode_ogg((tsf_u8*)*pRawBuffer, (tsf_u8*)*pRawBuffer + chunkSmpl->size, pFloatBuffer, &resNum, &resMax, resInitial)) return 0;
+	*pFloatBuffer = (float*)TSF_REALLOC(*pFloatBuffer, resNum * sizeof(float));
+	*pSmplCount = resNum;
+	return (*pFloatBuffer ? 1 : 0);
 	#else
-	// Allocate enough to hold the decoded float samples (see tsf_decode_samples)
-	*smplBuffer = (tsf_u8*)TSF_MALLOC(smplLength / sizeof(short) * sizeof(float));
+	// Inline convert the samples from short to float
+	float *res, *out; const short *in;
+	*pSmplCount = chunkSmpl->size / sizeof(short);
+	*pFloatBuffer = (float*)TSF_MALLOC(*pSmplCount * sizeof(float));
+	if (!*pFloatBuffer || !stream->read(stream->data, *pFloatBuffer, chunkSmpl->size)) return 0;
+	for (res = *pFloatBuffer, out = res + *pSmplCount, in = (short*)res + *pSmplCount; out != res;)
+		*(--out) = (float)(*(--in) / 32767.0);
+	return 1;
 	#endif
-	return (*smplBuffer ? stream->read(stream->data, *smplBuffer, smplLength) : 0);
 }
 
 static void tsf_voice_envelope_nextsegment(struct tsf_voice_envelope* e, short active_segment, float outSampleRate)
@@ -1334,8 +1350,9 @@
 	struct tsf_riffchunk chunkHead;
 	struct tsf_riffchunk chunkList;
 	struct tsf_hydra hydra;
-	tsf_u8* smplBuffer = TSF_NULL;
-	unsigned int smplLength = 0;
+	void* rawBuffer = TSF_NULL;
+	float* floatBuffer = TSF_NULL;
+	tsf_u32 smplCount = 0;
 
 	if (!tsf_riffchunk_read(TSF_NULL, &chunkHead, stream) || !TSF_FourCCEquals(chunkHead.id, "sfbk"))
 	{
@@ -1377,10 +1394,13 @@
 		{
 			while (tsf_riffchunk_read(&chunkList, &chunk, stream))
 			{
-				if (TSF_FourCCEquals(chunk.id, "smpl") && !smplBuffer && chunk.size >= sizeof(short))
+				if ((TSF_FourCCEquals(chunk.id, "smpl")
+						#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
+						|| TSF_FourCCEquals(chunk.id, "smpo")
+						#endif
+					) && !rawBuffer && !floatBuffer && chunk.size >= sizeof(short))
 				{
-					smplLength = chunk.size;
-					if (!tsf_load_samples(&smplBuffer, smplLength, stream)) goto out_of_memory;
+					if (!tsf_load_samples(&rawBuffer, &floatBuffer, &smplCount, &chunk, stream)) goto out_of_memory;
 				}
 				else stream->skip(stream->data, chunk.size);
 			}
@@ -1391,20 +1411,21 @@
 	{
 		//if (e) *e = TSF_INVALID_INCOMPLETE;
 	}
-	else if (smplBuffer == TSF_NULL)
+	else if (!rawBuffer && !floatBuffer)
 	{
 		//if (e) *e = TSF_INVALID_NOSAMPLEDATA;
 	}
 	else
 	{
-		float* fontSamples; unsigned int fontSampleCount;
-		if (!tsf_decode_samples(smplBuffer, smplLength, &fontSamples, &fontSampleCount, &hydra)) goto out_of_memory;
-		if (fontSamples == (float*)smplBuffer) smplBuffer = TSF_NULL; // Was converted inline, don't free below
+		#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
+		if (!floatBuffer && !tsf_decode_sf3_samples(rawBuffer, &floatBuffer, &smplCount, &hydra)) goto out_of_memory;
+		#endif
 		res = (tsf*)TSF_MALLOC(sizeof(tsf));
 		if (res) TSF_MEMSET(res, 0, sizeof(tsf));
-		if (!res || !tsf_load_presets(res, &hydra, fontSampleCount)) { TSF_FREE(fontSamples); goto out_of_memory; }
-		res->fontSamples = fontSamples;
+		if (!res || !tsf_load_presets(res, &hydra, smplCount)) goto out_of_memory;
 		res->outSampleRate = 44100.0f;
+		res->fontSamples = floatBuffer;
+		floatBuffer = TSF_NULL; // don't free below
 	}
 	if (0)
 	{
@@ -1416,7 +1437,7 @@
 	TSF_FREE(hydra.phdrs); TSF_FREE(hydra.pbags); TSF_FREE(hydra.pmods);
 	TSF_FREE(hydra.pgens); TSF_FREE(hydra.insts); TSF_FREE(hydra.ibags);
 	TSF_FREE(hydra.imods); TSF_FREE(hydra.igens); TSF_FREE(hydra.shdrs);
-	TSF_FREE(smplBuffer);
+	TSF_FREE(rawBuffer);   TSF_FREE(floatBuffer);
 	return res;
 }