SDL_mixer: fluidsynth: respect library samplerate limits and fix sample conversion

From 8e782637d61ad2fb35d401e4b6893ca1321e8bfb Mon Sep 17 00:00:00 2001
From: Ozkan Sezer <[EMAIL REDACTED]>
Date: Sun, 4 Apr 2021 03:51:56 +0300
Subject: [PATCH] fluidsynth: respect library samplerate limits and fix sample
 conversion

sample retrieval and AudioCVT code copied over from music_ogg.
fixes: https://bugzilla.libsdl.org/show_bug.cgi?id=3969  (corresponding
github bug: https://github.com/libsdl-org/SDL_mixer/issues/240)
---
 CHANGES            |  1 +
 music_fluidsynth.c | 85 ++++++++++++++++++++++++++++++----------------
 music_fluidsynth.h |  5 ++-
 3 files changed, 60 insertions(+), 31 deletions(-)

diff --git a/CHANGES b/CHANGES
index 049ba34..fd3f8ad 100644
--- a/CHANGES
+++ b/CHANGES
@@ -26,6 +26,7 @@
 - Native Midi (macosx): use newer apis with newer macOS (bug #1566)
 - Native Midi (macosx): set volume before start playing (bug #5356)
 - Native Midi (windows): fixed possible crash.
+- Midi (fluidsyth): respect synth.sample-rate limits (bug #3969)
 - Midi (fluidsyth): fixed loading MIDI music leaks memory (bug #3018)
 - Midi (fluidsyth): fixed calling Mix_Quit twice leading to segfault
   (bug #2004)
diff --git a/music_fluidsynth.c b/music_fluidsynth.c
index 8702c0d..c5b2a58 100644
--- a/music_fluidsynth.c
+++ b/music_fluidsynth.c
@@ -34,6 +34,8 @@ static Uint16 format;
 static Uint8 channels;
 static int freq;
 
+#define CVT_BUFSIZE 4096
+
 int SDLCALL fluidsynth_check_soundfont(const char *path, void *data)
 {
 	FILE *file = fopen(path, "r");
@@ -72,6 +74,7 @@ static FluidSynthMidiSong *fluidsynth_loadsong_common(SDL_RWops *rw)
 	off_t rw_offset;
 	size_t rw_size;
 	char *rw_mem;
+	int src_freq;
 	int ret;
 
 	if (!Mix_Init(MIX_INIT_FLUIDSYNTH)) {
@@ -85,17 +88,30 @@ static FluidSynthMidiSong *fluidsynth_loadsong_common(SDL_RWops *rw)
 
 	SDL_memset(song, 0, sizeof(FluidSynthMidiSong));
 
-	if (SDL_BuildAudioCVT(&song->convert, AUDIO_S16, 2, freq, format, channels, freq) < 0) {
+	/* fluidsynth limits: */
+	src_freq = freq;
+	if (src_freq < 8000) {
+		src_freq = 8000;
+	}
+	else if (src_freq > 96000) {
+		src_freq = 48000;
+	}
+	if (SDL_BuildAudioCVT(&song->convert, AUDIO_S16SYS, 2, src_freq, format, channels, freq) < 0) {
 		Mix_SetError("Failed to set up audio conversion");
 		goto fail;
 	}
+	song->convert.buf = SDL_malloc(CVT_BUFSIZE * song->convert.len_mult);
+	if (!song->convert.buf) {
+		Mix_SetError("Insufficient memory for song");
+		goto fail;
+	}
 
 	if (!(song->settings = fluidsynth.new_fluid_settings())) {
 		Mix_SetError("Failed to create FluidSynth settings");
 		goto fail;
 	}
 
-	fluidsynth.fluid_settings_setnum(song->settings, "synth.sample-rate", (double)freq);
+	fluidsynth.fluid_settings_setnum(song->settings, "synth.sample-rate", (double)src_freq);
 
 	if (!(song->synth = fluidsynth.new_fluid_synth(song->settings))) {
 		Mix_SetError("Failed to create FluidSynth synthesizer");
@@ -167,6 +183,7 @@ void fluidsynth_freesong(FluidSynthMidiSong *song)
 	if (song->settings) {
 		fluidsynth.delete_fluid_settings(song->settings);
 	}
+	SDL_free(song->convert.buf);
 	SDL_free(song);
 }
 
@@ -174,11 +191,13 @@ void fluidsynth_start(FluidSynthMidiSong *song)
 {
 	fluidsynth.fluid_player_set_loop(song->player, 1);
 	fluidsynth.fluid_player_play(song->player);
+	song->playing = 1;
 }
 
 void fluidsynth_stop(FluidSynthMidiSong *song)
 {
 	fluidsynth.fluid_player_stop(song->player);
+	song->playing = 0;
 }
 
 int fluidsynth_active(FluidSynthMidiSong *song)
@@ -192,43 +211,49 @@ void fluidsynth_setvolume(FluidSynthMidiSong *song, int volume)
 	fluidsynth.fluid_synth_set_gain(song->synth, (float) (volume * 1.2 / MIX_MAX_VOLUME));
 }
 
-int fluidsynth_playsome(FluidSynthMidiSong *song, void *dest, int dest_len)
+static void fluid_get_samples(FluidSynthMidiSong *song)
 {
-	int result = -1;
-	int frames = dest_len / channels / ((format & 0xFF) / 8);
-	int src_len = frames * 4; /* 16-bit stereo */
-	void *src = dest;
-
-	if (dest_len < src_len) {
-		if (!(src = SDL_malloc(src_len))) {
-			Mix_SetError("Insufficient memory for audio conversion");
-			return result;
-		}
-	}
+	Uint8 data[CVT_BUFSIZE];
+	SDL_AudioCVT *cvt;
 
-	if (fluidsynth.fluid_synth_write_s16(song->synth, frames, src, 0, 2, src, 1, 2) != FLUID_OK) {
+	if (fluidsynth.fluid_synth_write_s16(song->synth, CVT_BUFSIZE / 4, data, 0, 2, data, 1, 2) != FLUID_OK) {
+		fluidsynth_stop(song);
 		Mix_SetError("Error generating FluidSynth audio");
-		goto finish;
+		return;
 	}
 
-	song->convert.buf = src;
-	song->convert.len = src_len;
-
-	if (SDL_ConvertAudio(&song->convert) < 0) {
-		Mix_SetError("Error during audio conversion");
-		goto finish;
+	cvt = &song->convert;
+	memcpy(cvt->buf, data, CVT_BUFSIZE);
+	if (cvt->needed) {
+		cvt->len = CVT_BUFSIZE;
+		SDL_ConvertAudio(cvt);
+	} else {
+		cvt->len_cvt = CVT_BUFSIZE;
 	}
+	song->len_available = cvt->len_cvt;
+	song->snd_available = cvt->buf;
+}
 
-	if (src != dest)
-		memcpy(dest, src, dest_len);
-
-	result = 0;
+int fluidsynth_playsome(FluidSynthMidiSong *song, Uint8 *dest, int len)
+{
+	int mixable;
 
-finish:
-	if (src != dest)
-		SDL_free(src);
+	while (len > 0 && song->playing) {
+		if (!song->len_available) {
+			fluid_get_samples(song);
+		}
+		mixable = len;
+		if (mixable > song->len_available) {
+			mixable = song->len_available;
+		}
+		memcpy(dest, song->snd_available, mixable);
+		song->len_available -= mixable;
+		song->snd_available += mixable;
+		len -= mixable;
+		dest += mixable;
+	}
 
-	return result;
+	return len;
 }
 
 #endif /* USE_FLUIDSYNTH_MIDI */
diff --git a/music_fluidsynth.h b/music_fluidsynth.h
index 8c01e61..d053bac 100644
--- a/music_fluidsynth.h
+++ b/music_fluidsynth.h
@@ -36,6 +36,9 @@ typedef struct {
 	fluid_synth_t *synth;
 	fluid_settings_t *settings;
 	fluid_player_t* player;
+	int len_available;
+	Uint8 *snd_available;
+	int playing;
 } FluidSynthMidiSong;
 
 int fluidsynth_init(SDL_AudioSpec *mixer);
@@ -45,7 +48,7 @@ void fluidsynth_start(FluidSynthMidiSong *song);
 void fluidsynth_stop(FluidSynthMidiSong *song);
 int fluidsynth_active(FluidSynthMidiSong *song);
 void fluidsynth_setvolume(FluidSynthMidiSong *song, int volume);
-int fluidsynth_playsome(FluidSynthMidiSong *song, void *stream, int len);
+int fluidsynth_playsome(FluidSynthMidiSong *song, Uint8 *stream, int len);
 
 #endif /* USE_FLUIDSYNTH_MIDI */