shithub: sf2mid

Download patch

ref: fd6ab3039cbf1076678223303115e610a68d6329
parent: 2faf5b7adde4a263fe64ee94413c6aec326e5d07
author: Bernhard Schelling <[email protected]>
date: Thu Nov 2 22:51:08 EDT 2017

- New TinyMidiLoader library with new MIDI playback example
- Added tsf_note_off_all function
- Updated TinySoundFont version to 0.8

--- a/examples/build-linux-clang.sh
+++ b/examples/build-linux-clang.sh
@@ -1,5 +1,7 @@
 echo Building \'example1-linux-`uname -m`\' ...
-clang -Wall example1.c minisdl_audio.c -lm -ldl -lpthread -o example1-linux-`uname -m`
+clang -std=c99 -Wall example1.c minisdl_audio.c -lm -ldl -lpthread -o example1-linux-`uname -m`
 echo Building \'example2-linux-`uname -m`\' ...
-clang -Wall example2.c minisdl_audio.c -lm -ldl -lpthread -o example2-linux-`uname -m`
+clang -std=c99 -Wall example2.c minisdl_audio.c -lm -ldl -lpthread -o example2-linux-`uname -m`
+echo Building \'example3-linux-`uname -m`\' ...
+clang -std=c99 -Wall example3.c minisdl_audio.c -lm -ldl -lpthread -o example3-linux-`uname -m`
 echo Done!
--- a/examples/build-linux-gcc.sh
+++ b/examples/build-linux-gcc.sh
@@ -2,4 +2,6 @@
 gcc -Wall example1.c minisdl_audio.c -lm -ldl -lpthread -o example1-linux-`uname -m`
 echo Building \'example2-linux-`uname -m`\' ...
 gcc -Wall example2.c minisdl_audio.c -lm -ldl -lpthread -o example2-linux-`uname -m`
+echo Building \'example3-linux-`uname -m`\' ...
+gcc -Wall example3.c minisdl_audio.c -lm -ldl -lpthread -o example3-linux-`uname -m`
 echo Done!
--- a/examples/build-osx.sh
+++ b/examples/build-osx.sh
@@ -1,5 +1,7 @@
 echo Building \'example1-osx-`uname -m`\' ...
-clang -Wall example1.c minisdl_audio.c -lm -ldl -lpthread -framework CoreServices -framework CoreAudio -framework AudioUnit -o example1-osx-`uname -m`
+clang -std=c99 -Wall example1.c minisdl_audio.c -lm -ldl -lpthread -framework CoreServices -framework CoreAudio -framework AudioUnit -o example1-osx-`uname -m`
 echo Building \'example2-osx-`uname -m`\' ...
-clang -Wall example2.c minisdl_audio.c -lm -ldl -lpthread -framework CoreServices -framework CoreAudio -framework AudioUnit -o example2-osx-`uname -m`
+clang -std=c99 -Wall example2.c minisdl_audio.c -lm -ldl -lpthread -framework CoreServices -framework CoreAudio -framework AudioUnit -o example2-osx-`uname -m`
+echo Building \'example3-osx-`uname -m`\' ...
+clang -std=c99 -Wall example3.c minisdl_audio.c -lm -ldl -lpthread -framework CoreServices -framework CoreAudio -framework AudioUnit -o example3-osx-`uname -m`
 echo Done!
--- /dev/null
+++ b/examples/example3.c
@@ -1,0 +1,111 @@
+#include "minisdl_audio.h"
+
+#define TSF_IMPLEMENTATION
+#include "../tsf.h"
+
+#define TML_IMPLEMENTATION
+#include "../tml.h"
+
+// Holds the global instance pointer
+static tsf* g_TinySoundFont;
+
+// Holds global MIDI playback state
+static int g_MidiChannelPreset[16]; //active channel presets
+static double g_Msec;               //current playback time
+static tml_message* g_MidiMessage;  //next message to be played
+
+// Callback function called by the audio thread
+static void AudioCallback(void* data, Uint8 *stream, int len)
+{
+	//Number of samples to process
+	int SampleBlock, SampleCount = (len / (2 * sizeof(float))); //2 output channels
+	for (SampleBlock = TSF_RENDER_EFFECTSAMPLEBLOCK; SampleCount; SampleCount -= SampleBlock, stream += (SampleBlock * (2 * sizeof(float))))
+	{
+		//We progress the MIDI playback and then process TSF_RENDER_EFFECTSAMPLEBLOCK samples at once
+		if (SampleBlock > SampleCount) SampleBlock = SampleCount;
+
+		//Loop through all MIDI messages which need to be played up until the current playback time
+		for (g_Msec += SampleBlock * (1000.0 / 44100.0); g_MidiMessage && g_Msec >= g_MidiMessage->time; g_MidiMessage = g_MidiMessage->next)
+		{
+			switch (g_MidiMessage->type)
+			{
+				case TML_PROGRAM_CHANGE: //channel program (preset) change
+					g_MidiChannelPreset[g_MidiMessage->channel] = tsf_get_presetindex(g_TinySoundFont, 0, g_MidiMessage->program);
+					if (g_MidiChannelPreset[g_MidiMessage->channel] < 0) g_MidiChannelPreset[g_MidiMessage->channel] = 0;
+					break;
+				case TML_NOTE_ON: //play a note
+					tsf_note_on(g_TinySoundFont, g_MidiChannelPreset[g_MidiMessage->channel], g_MidiMessage->key, g_MidiMessage->velocity / 127.0f);
+					break;
+				case TML_NOTE_OFF: //stop a note
+					tsf_note_off(g_TinySoundFont, g_MidiChannelPreset[g_MidiMessage->channel], g_MidiMessage->key);
+					break;
+			}
+		}
+
+		// Render the block of audio samples in float format
+		tsf_render_float(g_TinySoundFont, (float*)stream, SampleBlock, 0);
+	}
+}
+
+int main(int argc, char *argv[])
+{
+	tml_message* TinyMidiLoader = NULL;
+
+	// Define the desired audio output format we request
+	SDL_AudioSpec OutputAudioSpec;
+	OutputAudioSpec.freq = 44100;
+	OutputAudioSpec.format = AUDIO_F32;
+	OutputAudioSpec.channels = 2;
+	OutputAudioSpec.samples = 4096;
+	OutputAudioSpec.callback = AudioCallback;
+
+	// Initialize the audio system
+	if (SDL_AudioInit(TSF_NULL) < 0)
+	{
+		fprintf(stderr, "Could not initialize audio hardware or driver\n");
+		return 1;
+	}
+
+	//Venture (Original WIP) by Ximon
+	//https://musescore.com/user/2391686/scores/841451
+	//License: Creative Commons copyright waiver (CC0)
+	TinyMidiLoader = tml_load_filename("venture.mid");
+	if (!TinyMidiLoader)
+	{
+		fprintf(stderr, "Could not load MIDI file\n");
+		return 1;
+	}
+
+	//Set up the global MidiMessage pointer to the first MIDI message
+	g_MidiMessage = TinyMidiLoader;
+
+	// Load the SoundFont from a file
+	g_TinySoundFont = tsf_load_filename("florestan-subset.sf2");
+	if (!g_TinySoundFont)
+	{
+		fprintf(stderr, "Could not load SoundFont\n");
+		return 1;
+	}
+
+	// Set the SoundFont rendering output mode with -18 db volume gain
+	tsf_set_output(g_TinySoundFont, TSF_STEREO_INTERLEAVED, OutputAudioSpec.freq, -18.0f);
+
+	// Request the desired audio output format
+	if (SDL_OpenAudio(&OutputAudioSpec, TSF_NULL) < 0)
+	{
+		fprintf(stderr, "Could not open the audio hardware or the desired audio output format\n");
+		return 1;
+	}
+
+	// Start the actual audio playback here
+	// The audio thread will begin to call our AudioCallback function
+	SDL_PauseAudio(0);
+
+	//Wait until the entire MIDI file has been played back (until the end of the linked message list is reached)
+	while (g_MidiMessage != NULL) SDL_Delay(100);
+
+	// We could call tsf_close(g_TinySoundFont) and tsf_free(TinyMidiLoader)
+	// here to free the memory and resources but we just let the OS clean up
+	// because the process ends here.
+	return 0;
+}
--- /dev/null
+++ b/examples/example3.dsp
@@ -1,0 +1,77 @@
+# Microsoft Developer Studio Project File - Name="example3" - Package Owner=<4>
+# Microsoft Developer Studio Generated Build File, Format Version 6.00
+
+# TARGTYPE "Win32 (x86) Console Application" 0x0103
+
+CFG=example3 - Win32 Debug
+!MESSAGE "example3 - Win32 Release" (based on  "Win32 (x86) Console Application")
+!MESSAGE "example3 - Win32 Debug" (based on  "Win32 (x86) Console Application")
+
+# Begin Project
+# PROP AllowPerConfigDependencies 0
+# PROP Scc_ProjName ""
+# PROP Scc_LocalPath ""
+CPP=cl.exe
+RSC=rc.exe
+
+!IF  "$(CFG)" == "example3 - Win32 Release"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 0
+# PROP BASE Output_Dir "Release"
+# PROP BASE Intermediate_Dir "Release"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 0
+# PROP Output_Dir "Release"
+# PROP Intermediate_Dir "Release"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c
+# ADD CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c
+# ADD BASE RSC /l 0x807 /d "NDEBUG"
+# ADD RSC /l 0x807 /d "NDEBUG"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib  kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386
+# ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib  kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386
+
+!ELSEIF  "$(CFG)" == "example3 - Win32 Debug"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 1
+# PROP BASE Output_Dir "Debug"
+# PROP BASE Intermediate_Dir "Debug"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 1
+# PROP Output_Dir "Debug"
+# PROP Intermediate_Dir "Debug"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /GZ  /c
+# ADD CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /GZ  /c
+# ADD BASE RSC /l 0x807 /d "_DEBUG"
+# ADD RSC /l 0x807 /d "_DEBUG"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib  kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept
+# ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib  kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept
+
+!ENDIF 
+
+# Begin Target
+# Name "example3 - Win32 Release"
+# Name "example3 - Win32 Debug"
+# Begin Source File
+SOURCE=example3.c
+# End Source File
+# Begin Source File
+SOURCE=minisdl_audio.c
+# End Source File
+# End Target
+# End Project
--- /dev/null
+++ b/examples/example3.vcxproj
@@ -1,0 +1,106 @@
+<?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>{78C3A807-00CB-410B-8F75-04910BB39760}</ProjectGuid>
+    <Keyword>Win32Proj</Keyword>
+    <RootNamespace>example3</RootNamespace>
+    <ProjectName>example3</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</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>
+    <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="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <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;%(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;%(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="minisdl_audio.c" />
+    <ClCompile Include="example3.c" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\tml.h" />
+    <ClInclude Include="minisdl_audio.h" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="..\tsf.h" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
--- a/examples/examples.sln
+++ b/examples/examples.sln
@@ -7,6 +7,8 @@
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "example2", "example2.vcxproj", "{78C3A807-00CB-410B-8F75-04910BB3975F}"
 EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "example3", "example3.vcxproj", "{78C3A807-00CB-410B-8F75-04910BB39760}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Win32 = Debug|Win32
@@ -31,6 +33,14 @@
 		{78C3A807-00CB-410B-8F75-04910BB3975F}.Release|Win32.Build.0 = Release|Win32
 		{78C3A807-00CB-410B-8F75-04910BB3975F}.Release|x64.ActiveCfg = Release|x64
 		{78C3A807-00CB-410B-8F75-04910BB3975F}.Release|x64.Build.0 = Release|x64
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Debug|Win32.ActiveCfg = Debug|Win32
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Debug|Win32.Build.0 = Debug|Win32
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Debug|x64.ActiveCfg = Debug|x64
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Debug|x64.Build.0 = Debug|x64
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Release|Win32.ActiveCfg = Release|Win32
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Release|Win32.Build.0 = Release|Win32
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Release|x64.ActiveCfg = Release|x64
+		{78C3A807-00CB-410B-8F75-04910BB39760}.Release|x64.Build.0 = Release|x64
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = TRUE
binary files /dev/null b/examples/venture.mid differ
--- /dev/null
+++ b/tml.h
@@ -1,0 +1,495 @@
+/* TinyMidiLoader - v0.7 - Minimalistic midi parsing library - https://github.com/schellingb/TinySoundFont
+                                     no warranty implied; use at your own risk
+   Do this:
+      #define TML_IMPLEMENTATION
+   before you include this file in *one* C or C++ file to create the implementation.
+   // i.e. it should look like this:
+   #include ...
+   #include ...
+   #define TML_IMPLEMENTATION
+   #include "tml.h"
+
+   [OPTIONAL] #define TML_NO_STDIO to remove stdio dependency
+   [OPTIONAL] #define TML_MALLOC, TML_REALLOC, and TML_FREE to avoid stdlib.h
+   [OPTIONAL] #define TML_MEMCPY to avoid string.h
+
+   LICENSE (ZLIB)
+
+   Copyright (C) 2017 Bernhard Schelling
+
+   This software is provided 'as-is', without any express or implied
+   warranty.  In no event will the authors be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this software must not be misrepresented; you must not
+      claim that you wrote the original software. If you use this software
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original software.
+   3. This notice may not be removed or altered from any source distribution.
+
+*/
+
+#ifndef TML_INCLUDE_TML_INL
+#define TML_INCLUDE_TML_INL
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Define this if you want the API functions to be static
+#ifdef TML_STATIC
+#define TMLDEF static
+#else
+#define TMLDEF extern
+#endif
+
+// Channel message type
+enum TMLMessageType
+{
+	TML_NOTE_OFF = 0x80, TML_NOTE_ON = 0x90, TML_KEY_PRESSURE = 0xA0, TML_CONTROL_CHANGE = 0xB0, TML_PROGRAM_CHANGE = 0xC0, TML_CHANNEL_PRESSURE = 0xD0, TML_PITCH_BEND = 0xE0
+};
+
+// Midi controller numbers
+enum TMLController
+{
+	TML_BANK_SELECT_MSB,      TML_MODULATIONWHEEL_MSB, TML_BREATH_MSB,       TML_FOOT_MSB = 4,      TML_PORTAMENTO_TIME_MSB,   TML_DATA_ENTRY_MSB, TML_VOLUME_MSB,
+	TML_BALANCE_MSB,          TML_PAN_MSB = 10,        TML_EXPRESSION_MSB,   TML_EFFECTS1_MSB,      TML_EFFECTS2_MSB,          TML_GPC1_MSB = 16, TML_GPC2_MSB, TML_GPC3_MSB, TML_GPC4_MSB,
+	TML_BANK_SELECT_LSB = 32, TML_MODULATIONWHEEL_LSB, TML_BREATH_LSB,       TML_FOOT_LSB = 36,     TML_PORTAMENTO_TIME_LSB,   TML_DATA_ENTRY_LSB, TML_VOLUME_LSB,
+	TML_BALANCE_LSB,          TML_PAN_LSB = 42,        TML_EXPRESSION_LSB,   TML_EFFECTS1_LSB,      TML_EFFECTS2_LSB,          TML_GPC1_LSB = 48, TML_GPC2_LSB, TML_GPC3_LSB, TML_GPC4_LSB,
+	TML_SUSTAIN_SWITCH = 64,  TML_PORTAMENTO_SWITCH,   TML_SOSTENUTO_SWITCH, TML_SOFT_PEDAL_SWITCH, TML_LEGATO_SWITCH,         TML_HOLD2_SWITCH,
+	TML_SOUND_CTRL1,          TML_SOUND_CTRL2,         TML_SOUND_CTRL3,      TML_SOUND_CTRL4,       TML_SOUND_CTRL5,           TML_SOUND_CTRL6,
+	TML_SOUND_CTRL7,          TML_SOUND_CTRL8,         TML_SOUND_CTRL9,      TML_SOUND_CTRL10,      TML_GPC5, TML_GPC6,        TML_GPC7, TML_GPC8,
+	TML_PORTAMENTO_CTRL,      TML_FX_REVERB = 91,      TML_FX_TREMOLO,       TML_FX_CHORUS,         TML_FX_CELESTE_DETUNE,     TML_FX_PHASER,
+	TML_DATA_ENTRY_INCR,      TML_DATA_ENTRY_DECR,     TML_NRPN_LSB,         TML_NRPN_MSB,          TML_RPN_LSB,               TML_RPN_MSB,
+	TML_ALL_SOUND_OFF = 120,  TML_ALL_CTRL_OFF,        TML_LOCAL_CONTROL,    TML_ALL_NOTES_OFF,     TML_OMNI_OFF, TML_OMNI_ON, TML_POLY_OFF, TML_POLY_ON
+};
+
+// A single MIDI message linked to the next message in time
+typedef struct tml_message
+{
+	// Time of the message in milliseconds
+	unsigned int time;
+
+	// Type (see TMLMessageType) and channel number
+	unsigned char type, channel;
+
+	// 2 byte of parameter data based on the type:
+	// - key, velocity for TML_NOTE_ON and TML_NOTE_OFF messages
+	// - key, key_pressure for TML_KEY_PRESSURE messages
+	// - control, control_value for TML_CONTROL_CHANGE messages (see TMLController)
+	// - program for TML_PROGRAM_CHANGE messages
+	// - channel_pressure for TML_CHANNEL_PRESSURE messages
+	// - pitch_bend for TML_PITCH_BEND messages
+	union
+	{
+		struct { union { char key, control, program, channel_pressure; }; union { char velocity, key_pressure, control_value; }; };
+		struct { unsigned short pitch_bend; };
+	};
+
+	// The pointer to the next message in time following this event
+	struct tml_message* next;
+} tml_message;
+
+// The load functions will return a pointer to a struct tml_message.
+// Normally the linked list gets traversed by following the next pointers.
+// Make sure to keep the pointer to the first message to free the memory.
+// On error the tml_load* functions will return NULL most likely due to an
+// invalid MIDI stream (or if the file did not exist in tml_load_filename).
+
+#ifndef TML_NO_STDIO
+// Directly load a MIDI file from a .mid file path
+TMLDEF tml_message* tml_load_filename(const char* filename);
+#endif
+
+// Load a MIDI file from a block of memory
+TMLDEF tml_message* tml_load_memory(const void* buffer, int size);
+
+// Get infos about this loaded MIDI file, returns the note count
+// NULL can be passed for any output value pointer if not needed.
+//   used_channels:   Will be set to how many channels play notes
+//                    (i.e. 1 if channel 15 is used but no other)
+//   used_programs:   Will be set to how many different programs are used
+//   total_notes:     Will be set to the total number of note on messages
+//   time_first_note: Will be set to the time of the first note on message
+//   time_length:     Will be set to the total time in milliseconds
+TMLDEF int tml_get_info(tml_message* first_message, int* used_channels, int* used_programs, int* total_notes, unsigned int* time_first_note, unsigned int* time_length);
+
+// Free all the memory of the linked message list (can also call free() manually)
+TMLDEF void tml_free(tml_message* f);
+
+// Stream structure for the generic loading
+struct tml_stream
+{
+	// Custom data given to the functions as the first parameter
+	void* data;
+
+	// Function pointer will be called to read 'size' bytes into ptr (returns number of read bytes)
+	int (*read)(void* data, void* ptr, unsigned int size);
+};
+
+// Generic Midi loading method using the stream structure above
+TMLDEF tml_message* tml_load(struct tml_stream* stream);
+TMLDEF tml_message* tml_load_tsf_stream(struct tsf_stream* stream) { return tml_load((struct tml_stream*)stream); }
+
+#ifdef __cplusplus
+}
+#endif
+
+// end header
+// ---------------------------------------------------------------------------------------------------------
+#endif //TML_INCLUDE_TML_INL
+
+#ifdef TML_IMPLEMENTATION
+
+#if !defined(TML_MALLOC) || !defined(TML_FREE) || !defined(TML_REALLOC)
+#  include <stdlib.h>
+#  define TML_MALLOC  malloc
+#  define TML_FREE    free
+#  define TML_REALLOC realloc
+#endif
+
+#if !defined(TML_MEMCPY)
+#  include <string.h>
+#  define TML_MEMCPY  memcpy
+#endif
+
+#define TML_NULL 0
+
+////crash on errors and warnings to find broken midi files while debugging
+//#define TML_ERROR(msg) *(int*)0 = 0xbad;
+//#define TML_WARN(msg)  *(int*)0 = 0xf00d;
+
+////print errors and warnings
+//#define TML_ERROR(msg) printf("ERROR: %s\n", msg);
+//#define TML_WARN(msg)  printf("WARNING: %s\n", msg);
+
+#ifndef TML_ERROR
+#define TML_ERROR(msg)
+#endif
+
+#ifndef TML_WARN
+#define TML_WARN(msg)
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef TML_NO_STDIO
+static int tml_stream_stdio_read(FILE* f, void* ptr, unsigned int size) { return (int)fread(ptr, 1, size, f); }
+TMLDEF tml_message* tml_load_filename(const char* filename)
+{
+	struct tml_message* res;
+	struct tml_stream stream = { TML_NULL, (int(*)(void*,void*,unsigned int))&tml_stream_stdio_read };
+	#if __STDC_WANT_SECURE_LIB__
+	FILE* f = TML_NULL; fopen_s(&f, filename, "rb");
+	#else
+	FILE* f = fopen(filename, "rb");
+	#endif
+	if (!f) { TML_ERROR("File not found"); return 0; }
+	stream.data = f;
+	res = tml_load(&stream);
+	fclose(f);
+	return res;
+}
+#endif
+
+struct tml_stream_memory { const char* buffer; unsigned int total, pos; };
+static int tml_stream_memory_read(struct tml_stream_memory* m, void* ptr, unsigned int size) { if (size > m->total - m->pos) size = m->total - m->pos; TML_MEMCPY(ptr, m->buffer+m->pos, size); m->pos += size; return size; }
+TMLDEF struct tml_message* tml_load_memory(const void* buffer, int size)
+{
+	struct tml_stream stream = { TML_NULL, (int(*)(void*,void*,unsigned int))&tml_stream_memory_read };
+	struct tml_stream_memory f = { 0, 0, 0 };
+	f.buffer = (const char*)buffer;
+	f.total = size;
+	stream.data = &f;
+	return tml_load(&stream);
+}
+
+struct tml_track
+{
+	unsigned int Idx, End, Ticks;
+};
+
+struct tml_tempomsg
+{
+	unsigned int time;
+	unsigned char type, Tempo[3];
+	tml_message* next;
+};
+
+struct tml_parser
+{
+	struct tml_stream *stream;
+	unsigned char *buf, *buf_end; 
+	int last_status, message_array_size, message_count;
+};
+
+enum TMLSystemType
+{
+	TML_TEXT  = 0x01, TML_COPYRIGHT = 0x02, TML_TRACK_NAME    = 0x03, TML_INST_NAME      = 0x04, TML_LYRIC         = 0x05, TML_MARKER          = 0x06, TML_CUE_POINT    = 0x07,
+	TML_EOT   = 0x2f, TML_SET_TEMPO = 0x51, TML_SMPTE_OFFSET  = 0x54, TML_TIME_SIGNATURE = 0x58, TML_KEY_SIGNATURE = 0x59, TML_SEQUENCER_EVENT = 0x7f,
+	TML_SYSEX = 0xf0, TML_TIME_CODE = 0xf1, TML_SONG_POSITION = 0xf2, TML_SONG_SELECT    = 0xf3, TML_TUNE_REQUEST  = 0xf6, TML_EOX             = 0xf7,
+	TML_SYNC  = 0xf8, TML_TICK      = 0xf9, TML_START         = 0xfa, TML_CONTINUE       = 0xfb, TML_STOP          = 0xfc, TML_ACTIVE_SENSING  = 0xfe, TML_SYSTEM_RESET = 0xff
+};
+
+int tml_readbyte(struct tml_parser* p)
+{
+	return (p->buf == p->buf_end ? -1 : *(p->buf++));
+}
+
+int tml_readvariablelength(struct tml_parser* p)
+{
+	unsigned int res = 0, i = 0;
+	unsigned char c;
+	for (; i != 4; i++)
+	{
+		if (p->buf == p->buf_end) { TML_WARN("Unexpected end of file"); return -1; }
+		c = *(p->buf++);
+		if (c & 0x80) res = ((res | (c & 0x7F)) << 7);
+		else return (int)(res | c);
+	}
+	TML_WARN("Invalid variable length byte count"); return -1;
+}
+
+static int tml_parsemessage(tml_message** f, struct tml_parser* p)
+{
+	int deltatime = tml_readvariablelength(p), status = tml_readbyte(p);
+	tml_message* evt;
+
+	if (deltatime & 0xFFF00000) deltatime = 0; //throw away delays that are insanely high for malformatted midis
+	if (status < 0) { TML_WARN("Unexpected end of file"); return -1; }
+	if ((status & 0x80) == 0)
+	{
+		// Invalid, use same status as before
+		if ((p->last_status & 0x80) == 0) { TML_WARN("Undefined status and invalid running status"); return -1; }
+		p->buf--;
+		status = p->last_status;
+	}
+	else p->last_status = status;
+
+	if (p->message_array_size == p->message_count)
+	{
+		//start allocated memory size of message array at 64, double each time until 8192, then add 1024 entries until done
+		p->message_array_size += (!p->message_array_size ? 64 : (p->message_array_size > 4096 ? 1024 : p->message_array_size));
+		*f = (tml_message*)TML_REALLOC(*f, p->message_array_size * sizeof(tml_message));
+		if (!*f) { TML_ERROR("Out of memory"); return -1; }
+	}
+	evt = *f + p->message_count;
+
+	//check what message we have
+	if ((status == TML_SYSEX) || (status == TML_EOX)) //sysex
+	{
+		//sysex messages are not handled
+		p->buf += tml_readvariablelength(p);
+		if (p->buf > p->buf_end) { TML_WARN("Unexpected end of file"); p->buf = p->buf_end; return -1; }
+		evt->type = 0;
+	}
+	else if (status == 0xFF) //meta events
+	{
+		int meta_type = tml_readbyte(p), buflen = tml_readvariablelength(p);
+		unsigned char* metadata = p->buf;
+		if (meta_type < 0) { TML_WARN("Unexpected end of file"); return -1; }
+		if (buflen > 0 && (p->buf += buflen) > p->buf_end) { TML_WARN("Unexpected end of file"); p->buf = p->buf_end; return -1; }
+
+		switch (meta_type)
+		{
+			case TML_EOT:
+				if (buflen != 0) { TML_WARN("Invalid length for EndOfTrack event"); return -1; }
+				if (!deltatime) return TML_EOT; //no need to store this message
+				evt->type = TML_EOT;
+				break;
+
+			case TML_SET_TEMPO:
+				if (buflen != 3) { TML_WARN("Invalid length for SetTempo meta event"); return -1; }
+				evt->type = TML_SET_TEMPO;
+				((struct tml_tempomsg*)evt)->Tempo[0] = metadata[0];
+				((struct tml_tempomsg*)evt)->Tempo[1] = metadata[1];
+				((struct tml_tempomsg*)evt)->Tempo[2] = metadata[2];
+				break;
+
+			default:
+				evt->type = 0;
+		}
+	}
+	else //channel message
+	{
+		int param; 
+		if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
+		evt->key = (param & 0x7f);
+		evt->channel = (status & 0x0f);
+		switch (evt->type = (status & 0xf0))
+		{
+			case TML_NOTE_OFF:
+			case TML_NOTE_ON:
+			case TML_KEY_PRESSURE:
+			case TML_CONTROL_CHANGE:
+				if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
+				evt->velocity = (param & 0x7f);
+				break;
+
+			case TML_PITCH_BEND:
+				if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
+				evt->pitch_bend = ((evt->key & 0x7f) << 7) | (param & 0x7f);
+				break;
+
+			case TML_PROGRAM_CHANGE:
+			case TML_CHANNEL_PRESSURE:
+				evt->velocity = 0;
+				break;
+
+			default: //ignore system/manufacture messages
+				evt->type = 0;
+				break;
+		}
+	}
+
+	if (deltatime || evt->type)
+	{
+		evt->time = deltatime;
+		p->message_count++;
+	}
+	return evt->type;
+}
+
+TMLDEF tml_message* tml_load(struct tml_stream* stream)
+{
+	int num_tracks, division, trackbufsize = 0;
+	unsigned char midi_header[15], *trackbuf = TML_NULL;
+	struct tml_message* messages = TML_NULL;
+	struct tml_track *tracks, *t, *tracksEnd;
+	struct tml_parser p = { stream };
+
+	// Parse MIDI header
+	if (stream->read(stream->data, midi_header, 14) != 14) { TML_ERROR("Unexpected end of file"); return messages; }
+	if (midi_header[0] != 'M' || midi_header[1] != 'T' || midi_header[2] != 'h' || midi_header[3] != 'd' ||
+	    midi_header[7] != 6   || midi_header[9] >  2) { TML_ERROR("Doesn't look like a MIDI file: invalid MThd header"); return messages; }
+	if (midi_header[12] & 0x80) { TML_ERROR("File uses unsupported SMPTE timing"); return messages; }
+	num_tracks = (int)(midi_header[10] << 8) | midi_header[11];
+	division = (int)(midi_header[12] << 8) | midi_header[13]; //division is ticks per beat (quarter-note)
+	if (num_tracks <= 0 && division <= 0) { TML_ERROR("Doesn't look like a MIDI file: invalid track or division values"); return messages; }
+
+	// Allocate temporary tracks array for parsing
+	tracks = (struct tml_track*)TML_MALLOC(sizeof(struct tml_track) * num_tracks);
+	tracksEnd = &tracks[num_tracks];
+	for (t = tracks; t != tracksEnd; t++) t->Idx = t->End = t->Ticks = 0;
+
+	// Read all messages for all tracks
+	for (t = tracks; t != tracksEnd; t++)
+	{
+		unsigned char track_header[8];
+		int track_length;
+		if (stream->read(stream->data, track_header, 8) != 8) { TML_WARN("Unexpected end of file"); break; }
+		if (track_header[0] != 'M' || track_header[1] != 'T' || track_header[2] != 'r' || track_header[3] != 'k')
+			{ TML_WARN("Invalid MTrk header"); break; }
+
+		// Get size of track data and read into buffer (allocate bigger buffer if needed)
+		track_length = track_header[7] | (track_header[6] << 8) | (track_header[5] << 16) | (track_header[4] << 24);
+		if (track_length < 0) { TML_WARN("Invalid MTrk header"); break; }
+		if (trackbufsize < track_length) { TML_FREE(trackbuf); trackbuf = (unsigned char*)TML_MALLOC(trackbufsize = track_length); }
+		if (stream->read(stream->data, trackbuf, track_length) != track_length) { TML_WARN("Unexpected end of file"); break; }
+
+		t->Idx = p.message_count;
+		for (p.buf_end = (p.buf = trackbuf) + track_length; p.buf != p.buf_end;)
+		{
+			int type = tml_parsemessage(&messages, &p);
+			if (type == TML_EOT || type < 0) break; //file end or illegal data encountered
+		}
+		if (p.buf != p.buf_end) { TML_WARN( "Track length did not match data length"); }
+		t->End = p.message_count;
+	}
+	TML_FREE(trackbuf);
+
+	// Change message time signature from delta ticks to actual msec values and link messages ordered by time
+	if (p.message_count)
+	{
+		tml_message *PrevMessage = TML_NULL, *Msg, *MsgEnd, Swap;
+		unsigned int ticks = 0, tempo_ticks = 0; //tick counter and value at last tempo change
+		int step_smallest, msec, tempo_msec = 0; //msec value at last tempo change
+		double ticks2time = 480000 / (1000.0 * division); //milliseconds per tick
+
+		// Loop through all messages over all tracks ordered by time
+		for (step_smallest = 0; step_smallest != 0x7fffffff; ticks += step_smallest)
+		{
+			step_smallest = 0x7fffffff;
+			msec = tempo_msec + (int)((ticks - tempo_ticks) * ticks2time);
+			for (t = tracks; t != tracksEnd; t++)
+			{
+				if (t->Idx == t->End) continue;
+				for (Msg = &messages[t->Idx], MsgEnd = &messages[t->End]; Msg != MsgEnd && t->Ticks + Msg->time == ticks; Msg++, t->Idx++)
+				{
+					t->Ticks += Msg->time;
+					if (Msg->type == TML_SET_TEMPO)
+					{
+						unsigned char* Tempo = ((struct tml_tempomsg*)Msg)->Tempo;
+						ticks2time = ((Tempo[0]<<16)|(Tempo[1]<<8)|Tempo[2])/(1000.0 * division);
+						tempo_msec = msec;
+						tempo_ticks = ticks;
+					}
+					else if (Msg->type)
+					{
+						Msg->time = msec;
+						if (PrevMessage) { PrevMessage->next = Msg; PrevMessage = Msg; }
+						else { Swap = *Msg; *Msg = *messages; *messages = Swap; PrevMessage = messages; }
+					}
+				}
+				if (Msg != MsgEnd && t->Ticks + Msg->time > ticks)
+				{
+					int step = (int)(t->Ticks + Msg->time - ticks);
+					if (step < step_smallest) step_smallest = step;
+				}
+			}
+		}
+		if (PrevMessage) PrevMessage->next = TML_NULL;
+		else p.message_count = 0;
+	}
+	TML_FREE(tracks);
+
+	if (p.message_count == 0)
+	{
+		TML_FREE(messages);
+		messages = TML_NULL;
+	}
+
+	return messages;
+}
+
+TMLDEF int tml_get_info(tml_message* Msg, int* out_used_channels, int* out_used_programs, int* out_total_notes, unsigned int* out_time_first_note, unsigned int* out_time_length)
+{
+	int used_programs = 0, used_channels = 0, total_notes = 0;
+	unsigned int time_first_note = 0xffffffff, time_length = 0;
+	unsigned char channels[16] = { 0 }, programs[128] = { 0 };
+	for (;Msg; Msg = Msg->next)
+	{
+		time_length = Msg->time;
+		if (Msg->type == TML_PROGRAM_CHANGE && !programs[(int)Msg->program]) { programs[(int)Msg->program] = 1; used_programs++; }
+		if (Msg->type != TML_NOTE_ON) continue;
+		if (time_first_note == 0xffffffff) time_first_note = time_length;
+		if (!channels[Msg->channel]) { channels[Msg->channel] = 1; used_channels++; }
+		total_notes++;
+	}
+	if (time_first_note == 0xffffffff) time_first_note = 0;
+	if (out_used_channels  ) *out_used_channels   = used_channels;
+	if (out_used_programs  ) *out_used_programs   = used_programs;
+	if (out_total_notes    ) *out_total_notes     = total_notes;
+	if (out_time_first_note) *out_time_first_note = time_first_note;
+	if (out_time_length    ) *out_time_length     = time_length;
+	return total_notes;
+}
+
+TMLDEF void tml_free(tml_message* f)
+{
+	TML_FREE(f);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //TML_IMPLEMENTATION
--- a/tsf.h
+++ b/tsf.h
@@ -1,4 +1,4 @@
-/* TinySoundFont - v0.7 - SoundFont2 synthesizer - https://github.com/schellingb/TinySoundFont
+/* TinySoundFont - v0.8 - SoundFont2 synthesizer - https://github.com/schellingb/TinySoundFont
                                      no warranty implied; use at your own risk
    Do this:
       #define TSF_IMPLEMENTATION
@@ -19,6 +19,7 @@
      - Support for ChorusEffectsSend and ReverbEffectsSend generators
      - Better low-pass filter without lowering performance too much
      - Support for modulators
+     - Channel interface to better support MIDI playback
 
    LICENSE (MIT)
 
@@ -146,6 +147,9 @@
 TSFDEF void tsf_note_off(tsf* f, int preset_index, int key);
 TSFDEF void tsf_bank_note_off(tsf* f, int bank, int preset_number, int key);
 
+// Stop playing all notes
+TSFDEF void tsf_note_off_all(tsf* f);
+
 // Render output samples into a buffer
 // You can either render as signed 16-bit values (tsf_render_short) or
 // as 32-bit float values (tsf_render_float)
@@ -896,7 +900,7 @@
 	float tmpModLfoToPitch, tmpVibLfoToPitch, tmpModEnvToPitch;
 
 	TSF_BOOL dynamicGain = (region->modLfoToVolume != 0);
-	float noteGain, tmpModLfoToVolume;
+	float noteGain = 0, tmpModLfoToVolume;
 
 	if (dynamicLowpass) tmpSampleRate = f->outSampleRate, tmpInitialFilterFc = (float)region->initialFilterFc, tmpModLfoToFilterFc = (float)region->modLfoToFilterFc, tmpModEnvToFilterFc = (float)region->modEnvToFilterFc;
 	else tmpSampleRate = 0, tmpInitialFilterFc = 0, tmpModLfoToFilterFc = 0, tmpModEnvToFilterFc = 0;
@@ -1258,6 +1262,13 @@
 TSFDEF void tsf_bank_note_off(tsf* f, int bank, int preset_number, int key)
 {
 	tsf_note_off(f, tsf_get_presetindex(f, bank, preset_number), key);
+}
+
+TSFDEF void tsf_note_off_all(tsf* f)
+{
+	struct tsf_voice *v = f->voices, *vEnd = v + f->voiceNum;
+	for (; v != vEnd; v++) if (v->playingPreset != -1 && v->ampenv.segment < TSF_SEGMENT_RELEASE)
+		tsf_voice_end(v, f->outSampleRate);
 }
 
 TSFDEF void tsf_render_short(tsf* f, short* buffer, int samples, int flag_mixing)