shithub: sf2mid

Download patch

ref: a71934dbd129bfd445c45e716a2635773473e37f
parent: ad9b00f65a7810370908ed56e188b5a4ed61e14e
parent: b3b1e3c9ce9e869aa64ed9f2bb513a7ff9a18fea
author: qwx <qwx@sciops.net>
date: Thu Aug 10 02:03:55 EDT 2023

merge upstream

--- a/README.md
+++ b/README.md
@@ -1,37 +1,43 @@
-# TinySoundFont
-SoundFont2 synthesizer library in a single C/C++ file
-
-## Overview
-
-TinySoundFont is a software synthesizer using SoundFont2 sound bank files.
-
-The library is a single C header file so it is extremely simple to integrate in your C/C++ projects.
-
-```c++
-#define TSF_IMPLEMENTATION
-#include "tsf.h"
-
-...
-
-tsf* TinySoundFont = tsf_load_filename("soundfont.sf2");
-tsf_set_output(TinySoundFont, TSF_MONO, 44100, 0); //sample rate
-tsf_note_on(TinySoundFont, 0, 60, 1.0f); //preset 0, middle C
-short HalfSecond[22050]; //synthesize 0.5 seconds
-tsf_render_short(TinySoundFont, HalfSecond, 22050, 0);
-```
-
-The library code is based on [SFZero by Steve Folta](https://github.com/stevefolta/SFZero).
-
-## Documentation
-
-The API documentation can be found on [top of the library source code](https://github.com/schellingb/TinySoundFont/blob/master/tsf.h).
-
-There are also [examples available](https://github.com/schellingb/TinySoundFont/tree/master/examples) which come with a sample SoundFont file and build and play sound on Win32, Win64, Linux and MacOSX with no further dependencies.
-
-## Dependencies
-
-C standard libraries for fopen, math and malloc (can be removed by providing custom functions with #defines).
-
-## License
-
-TinySoundFont is available under the [MIT license](https://choosealicense.com/licenses/mit/).
+# sf2mid: TinySoundFont-based midi player using soundfont2 banks
+
+## Usage
+
+Recommended soundfont: Patch93's Roland SC55 font.
+
+Example usage: playing a doom midi file
+
+	; games/wadfs /sys/games/lib/doom/doom1.wad
+	createfile SW18_7: file already exists
+	; games/mus </mnt/wad/d_e1m3 \
+		| games/sf2mid /lib/midi/sf2/patch93.sc-55.sf2 >/dev/audio
+
+See: [mus(1)](http://man.9front.org/1/mus).
+
+Example usage: using sf2mid for playback in doom(1):
+
+	Edit dmus(1):
+	; cat /bin/dmus
+	#!/bin/rc
+	#sf2=sc55.v3.7.sf2
+	sf2=patch93.sc-55.sf2
+	if(test -f /lib/midi/sf2/$sf2)
+		c=(games/sf2mid /lib/midi/sf2/$sf2)
+	if not if(test -f /tmp/genmidi.*)
+		c=(games/dmid -i /tmp/genmidi.* '|' games/opl3)
+	if not
+		c=(games/midi -c)
+	if(~ `{file -m $1} audio/mus)
+		c=(games/mus '<' $1 '|' $c)
+	if not
+		c=('<' $1 $c)
+	eval $c
+
+Current port-specific issues: performance is not great;
+doom will hang while sf2mid unscrews itself.
+
+## Library
+
+See [TinySoundFont library](https://github.com/schellingb/TinySoundFont)
+for more information.
+
+License: MIT
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,16 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin/games
+TARG=sf2mid
+OFILES=\
+	sf2mid.$O\
+
+HFILES=\
+	tml.h\
+	tsf.h\
+
+CC=pcc
+#CFLAGS=-vwc
+CFLAGS=$CFLAGS -c -p -D_POSIX_SOURCE
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/sf2mid.c
@@ -1,0 +1,99 @@
+/* adapted from examples */
+#define TSF_IMPLEMENTATION
+#include "tsf.h"
+#define TML_IMPLEMENTATION
+#include "tml.h"
+
+#include <unistd.h>
+
+tsf *ttsf;
+double T;	/* msec */
+tml_message* next;
+
+enum{
+	Sampsz = 2 * 2,
+	Delay = 1764,	/* 40 ms */
+	Rate = 44100,
+};
+
+static int
+samp(void*, tsf_s16 *stream, int samp)
+{
+	int chunk;
+
+	samp *= 2;
+	for(chunk=2048; samp!=0; samp-=chunk){
+		if(chunk > samp)
+			chunk = samp;
+		T += (1000.0 / Rate) * chunk / 2;
+		for(; next && T >= next->time; next = next->next){
+			switch(next->type){
+				case TML_PROGRAM_CHANGE:
+					tsf_channel_set_presetnumber(ttsf, next->channel,
+						next->program, next->channel == 9);
+					break;
+				case TML_NOTE_ON:
+					tsf_channel_note_on(ttsf, next->channel,
+						next->key, next->velocity / 127.0);
+					break;
+				case TML_NOTE_OFF:
+					tsf_channel_note_off(ttsf, next->channel,
+						next->key);
+					break;
+				case TML_PITCH_BEND:
+					tsf_channel_set_pitchwheel(ttsf, next->channel,
+						next->pitch_bend);
+					break;
+				case TML_CONTROL_CHANGE:
+					tsf_channel_midi_control(ttsf, next->channel,
+						next->control, next->control_value);
+					break;
+			}
+		}
+		if(next == NULL)
+			return -1;
+		tsf_render_short(ttsf, stream, chunk, 0);
+		stream += chunk;
+	}
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	FILE *out;
+	char *sf, *mid;
+	tml_message* tml;
+	tsf_s16 obuf[Sampsz * Delay];
+
+	sf = NULL;
+	mid = "/fd/0";
+	out = stdout;
+	if(argc < 2){
+		fprintf(stderr, "usage: %s SF2BANK [FILE]\n", argv[0]);
+		return 1;
+	}
+	if(argc >= 2)
+		sf = argv[1];
+	if(argc >= 3)
+		mid = argv[2];
+	if((tml = tml_load_filename(mid)) == NULL){
+		fprintf(stderr, "error reading midi file\n");
+		return 1;
+	}
+	if((ttsf = tsf_load_filename(sf)) == NULL){
+		fprintf(stderr, "error loading soundfont\n");
+		return 2;
+	}
+	next = tml;
+	// arm 10th channel for percussion sound bank (128) if available
+	tsf_channel_set_bank_preset(ttsf, 9, 128, -3.0);
+	tsf_set_output(ttsf, TSF_STEREO_UNWEAVED, Rate, 0.0);
+	for(;;){
+		memset(obuf, 0, sizeof obuf);
+		if(samp(NULL, obuf, Delay) < 0)
+			break;
+		fwrite(obuf, Delay, 4, out);
+	}
+	return 0;
+}
--- a/tsf.h
+++ b/tsf.h
@@ -384,6 +384,7 @@
 
 enum { TSF_SEGMENT_NONE, TSF_SEGMENT_DELAY, TSF_SEGMENT_ATTACK, TSF_SEGMENT_HOLD, TSF_SEGMENT_DECAY, TSF_SEGMENT_SUSTAIN, TSF_SEGMENT_RELEASE, TSF_SEGMENT_DONE };
 
+#pragma pack on
 struct tsf_hydra
 {
 	struct tsf_hydra_phdr *phdrs; struct tsf_hydra_pbag *pbags; struct tsf_hydra_pmod *pmods;
@@ -471,6 +472,7 @@
 	int channelNum, activeChannel;
 	struct tsf_channel channels[1];
 };
+#pragma pack off
 
 static double tsf_timecents2Secsd(double timecents) { return TSF_POW(2.0, timecents / 1200.0); }
 static float tsf_timecents2Secsf(float timecents) { return TSF_POWF(2.0f, timecents / 1200.0f); }
@@ -544,7 +546,7 @@
 		GEN_FLOAT_MAX1000    = 0xB0, //min 0, max 1000
 		GEN_FLOAT_MAX1440    = 0xC0, //min 0, max 1440
 
-		_GEN_MAX = 59
+		_GEN_MAX = 59,
 	};
 	#define _TSFREGIONOFFSET(TYPE, FIELD) (unsigned char)(((TYPE*)&((struct tsf_region*)0)->FIELD) - (TYPE*)0)
 	#define _TSFREGIONENVOFFSET(TYPE, ENV, FIELD) (unsigned char)(((TYPE*)&((&(((struct tsf_region*)0)->ENV))->FIELD)) - (TYPE*)0)
@@ -825,7 +827,6 @@
 								zoneRegion.loop_start += pshdr->startLoop;
 								zoneRegion.loop_end += pshdr->endLoop;
 								if (pshdr->endLoop > 0) zoneRegion.loop_end -= 1;
-								if (zoneRegion.loop_end > fontSampleCount) zoneRegion.loop_end = fontSampleCount;
 								if (zoneRegion.pitch_keycenter == -1) zoneRegion.pitch_keycenter = pshdr->originalPitch;
 								zoneRegion.tune += pshdr->pitchCorrection;
 								zoneRegion.sample_rate = pshdr->sampleRate;
@@ -862,91 +863,57 @@
 	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 int tsf_decode_samples_ogg(tsf_u8* smplBuffer, tsf_u32 smplLength, float** outSamples, unsigned int* outSampleCount, struct tsf_hydra *hydra)
 {
-	#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
 	float *res = TSF_NULL;
-	tsf_u32 resNum = 0, resMax = 0, resInitial = (smplLength > 0x100000 ? (smplLength & ~0xFFFFF) : 65536);
+	unsigned int resNum = 0, resMax = 0, resInitial = (smplLength > 0x100000 ? (smplLength & ~0xFFFFF) : 65536);
 	int i;
 	for (i = 0; i < hydra->shdrNum; i++)
 	{
+		stb_vorbis *v;
 		struct tsf_hydra_shdr *shdr = &hydra->shdrs[i];
-		if (shdr->end <= shdr->start) continue;
-		if (shdr->sampleType & 0x30) // compression flags (sometimes Vorbis flag)
+		const tsf_u8 *pSmpl = smplBuffer + shdr->start, *pSmplEnd = smplBuffer + shdr->end;
+		if (pSmplEnd <= pSmpl) 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 (;;)
 		{
-			stb_vorbis *v;
-			const tsf_u8 *pSmpl = smplBuffer + shdr->start, *pSmplEnd = smplBuffer + shdr->end;
-			if (!TSF_FourCCEquals(pSmpl, "OggS"))
-			{
-				shdr->start = shdr->end = shdr->startLoop = shdr->endLoop = 0;
-				continue;
-			}
+			float** outputs; int n_samples;
 
-			// Use whatever stb_vorbis API that is available (either pull or push)
+			// 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)
-			v = stb_vorbis_open_memory(pSmpl, (int)(pSmplEnd - pSmpl), TSF_NULL, TSF_NULL);
+			n_samples = stb_vorbis_get_frame_float(v, TSF_NULL, &outputs);
+			if (!n_samples) break;
 			#else
-			{ int use, err; v = stb_vorbis_open_pushdata(pSmpl, (int)(pSmplEnd - pSmpl), &use, &err, TSF_NULL); pSmpl += use; }
+			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
-			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));
-			}
-			shdr->end = resNum;
-			stb_vorbis_close(v);
-		}
-		else // raw PCM sample
-		{
-			tsf_u32 fix_offset = resNum - shdr->start;
-			tsf_u32 n_samples = ((shdr->startLoop < shdr->endLoop && shdr->endLoop > shdr->startLoop) ? shdr->endLoop : shdr->end) - shdr->start;
-			float *out; short* in = (short*)smplBuffer + shdr->start, *inEnd = in + n_samples;
-			if ((tsf_u8*)inEnd > (smplBuffer + smplLength)) inEnd = (short*)smplBuffer + smplLength/sizeof(short);
-
-			// Fix up sample indices in shdr (end index is set after decoding)
-			shdr->start = resNum;
-			shdr->end += fix_offset;
-			shdr->startLoop += fix_offset;
-			shdr->endLoop += fix_offset;
-
-			// expand our output buffer if necessary then convert the PCM data from short to float
+			// 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) { return 0; }
+				if (!res) { stb_vorbis_close(v); return 0; }
 			}
-
-			// Convert the samples from short to float
-			for (out = res + resNum - n_samples; in != inEnd;)
-				*(out++) = (float)(*(in++) / 32767.0);
+			TSF_MEMCPY(res + resNum - n_samples, outputs[0], n_samples * sizeof(float));
 		}
+		shdr->end = resNum;
+		stb_vorbis_close(v);
 	}
 
 	// Trim the sample buffer down then return success (unless out of memory)
@@ -954,26 +921,47 @@
 	*outSamples = res;
 	*outSampleCount = resNum;
 	return (res ? 1 : 0);
-	#else
-	// Inline convert the samples from short to float (buffer was allocated big enough in tsf_load_samples)
+}
+#endif
+
+static int tsf_decode_samples(tsf_u8* smplBuffer, tsf_u32 smplLength, float** outSamples, unsigned int* outSampleCount, struct tsf_hydra *hydra)
+{
 	float *out; const short *in;
+
+	#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
+	if (TSF_FourCCEquals(smplBuffer, "OggS"))
+		return tsf_decode_samples_ogg(smplBuffer, smplLength, outSamples, outSampleCount, hydra);
+	#endif
+
+	// Inline convert the samples from short to float (buffer was allocated big enough in tsf_load_samples)
 	*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
 }
 
 static int tsf_load_samples(tsf_u8** smplBuffer, tsf_u32 smplLength, 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);
-	#else
+	// With OGG Vorbis support scan for a specific 4 byte sample header first
+	if (smplLength >= sizeof(tsf_fourcc))
+	{
+		// If the format is not OGG the buffer is made large enough to hold the decoded float samples
+		tsf_fourcc format;
+		stream->read(stream->data, &format, sizeof(tsf_fourcc));
+		if (TSF_FourCCEquals(format, "OggS"))
+			*smplBuffer = (tsf_u8*)TSF_MALLOC(smplLength);
+		else
+			*smplBuffer = (tsf_u8*)TSF_MALLOC(smplLength / sizeof(short) * sizeof(float));
+		if (!*smplBuffer) return 0;
+		memcpy(*smplBuffer, &format, sizeof(tsf_fourcc));
+		return stream->read(stream->data, (char*)*smplBuffer + sizeof(tsf_fourcc), smplLength - sizeof(tsf_fourcc));
+	}
+	#endif
+
 	// Allocate enough to hold the decoded float samples (see tsf_decode_samples)
 	*smplBuffer = (tsf_u8*)TSF_MALLOC(smplLength / sizeof(short) * sizeof(float));
-	#endif
 	return (*smplBuffer ? stream->read(stream->data, *smplBuffer, smplLength) : 0);
 }
 
@@ -1184,7 +1172,7 @@
 	double adjustedPitch = v->region->pitch_keycenter + (note - v->region->pitch_keycenter) * (v->region->pitch_keytrack / 100.0);
 	if (pitchShift) adjustedPitch += pitchShift;
 	v->pitchInputTimecents = adjustedPitch * 100.0;
-	v->pitchOutputFactor = v->region->sample_rate / (tsf_timecents2Secsd(v->region->pitch_keycenter * 100.0) * outSampleRate);
+	v->pitchOutputFactor = v->region->sample_rate / (tsf_timecents2Secsd(v->region->pitch_keycenter * 100.0) * outSampleRate * 2.0f);
 }
 
 static void tsf_voice_render(tsf* f, struct tsf_voice* v, float* outputBuffer, int numSamples)
@@ -1658,7 +1646,7 @@
 	while (samples > 0)
 	{
 		int channelSamples = (samples > maxChannelSamples ? maxChannelSamples : samples);
-		short* bufferEnd = buffer + channelSamples * channels;
+		short* bufferEnd = buffer + channelSamples;// * channels;
 		float *floatSamples = outputSamples;
 		tsf_render_float(f, floatSamples, channelSamples, TSF_FALSE);
 		samples -= channelSamples;