/* * * Singe 3 * Copyright (C) 2006-2026 Scott Duensing * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. * */ // Singe: MIDI through TinySoundFont. SDL_mixer's own MIDI decoder is TiMidity, which wants a GUS // patch set nobody installs any more; this reads a SoundFont instead, which is what a person who // wants MIDI to sound like something will already have. There is no MIDI hardware here in either // direction: a file is synthesised to samples at load, and that is all. #include #include #include "common.h" #include "decode.h" #include "midi.h" #include "util.h" #include "vfs.h" #define TSF_IMPLEMENTATION #include "../thirdparty/tinysoundfont/tsf.h" #define TML_IMPLEMENTATION #include "../thirdparty/tinysoundfont/tml.h" #define MIDI_SAMPLE_RATE 44100 #define MS_PER_SECOND 1000 #define MIDI_CHANNELS 2 #define MIDI_BLOCK_SAMPLES 512 // Messages land on a block boundary, 11.6 ms apart #define MIDI_TAIL_MS 3000 // Rendered past the last message so releases finish #define MIDI_MAX_MS (20 * 60 * 1000) // A MIDI longer than this is a mistake, not music #define MIDI_MAX_VOICES 64 #define MIDI_HEADER_BYTES 4 #define MIDI_DRUM_CHANNEL 9 #define MIDI_VELOCITY_MAX 127.0f #define MIDI_GAIN_DB (-3.0f) // A little headroom: many banks clip at full scale static const char *_paths[] = { // The support folder the engine extracts, so a game can carry its own bank, and a bare file // beside the game for someone who dropped one in. "Singe/soundfont.sf2", "soundfont.sf2", // Where the distributions put the banks their own MIDI players use. "/usr/share/soundfonts/default.sf2", "/usr/share/soundfonts/FluidR3_GM.sf2", "/usr/share/sounds/sf2/default-GM.sf2", "/usr/share/sounds/sf2/FluidR3_GM.sf2", "/usr/share/sounds/sf2/TimGM6mb.sf2" }; static tsf *_soundfont = NULL; static char *_found = NULL; static void _apply(tml_message *message); static bool _load(const char *path); // ===== Internal helpers ===== // One MIDI message into the synthesiser. Anything else in the file is nothing a synthesiser acts on. static void _apply(tml_message *message) { switch (message->type) { case TML_PROGRAM_CHANGE: tsf_channel_set_presetnumber(_soundfont, message->channel, message->program, (message->channel == MIDI_DRUM_CHANNEL)); break; case TML_NOTE_ON: tsf_channel_note_on(_soundfont, message->channel, message->key, (float)message->velocity / MIDI_VELOCITY_MAX); break; case TML_NOTE_OFF: tsf_channel_note_off(_soundfont, message->channel, message->key); break; case TML_PITCH_BEND: tsf_channel_set_pitchwheel(_soundfont, message->channel, message->pitch_bend); break; case TML_CONTROL_CHANGE: tsf_channel_midi_control(_soundfont, message->channel, message->control, message->control_value); break; default: break; } } // Reads one candidate. The vfs is asked first, so a packed game's own bank is found and a name is // resolved the way every other name in a game is; the file system is the fallback, which is what // finds the bank a distribution installed somewhere absolute. Asking in that order rather than // looking at the shape of the path keeps it working on Windows, where an absolute path does not // begin with a slash. static bool _load(const char *path) { size_t size = 0; char *bytes = vfsRead(path, &size); if (bytes != NULL) { if (size <= INT32_MAX) { _soundfont = tsf_load_memory(bytes, (int32_t)size); } free(bytes); } else { _soundfont = tsf_load_filename(path); } if (_soundfont == NULL) { return false; } tsf_set_max_voices(_soundfont, MIDI_MAX_VOICES); tsf_set_output(_soundfont, TSF_STEREO_INTERLEAVED, MIDI_SAMPLE_RATE, MIDI_GAIN_DB); _found = utilCreateString("%s", path); return true; } // ===== Public ===== void midiInit(const char *wanted) { int32_t x; if (wanted != NULL) { if (!_load(wanted)) { utilTrace("No MIDI sound bank: %s could not be read as a SoundFont.", wanted); } return; } for (x = 0; x < (int32_t)(sizeof(_paths) / sizeof(_paths[0])); x++) { if (_load(_paths[x])) { utilTrace("MIDI sound bank: %s, %d presets.", _found, tsf_get_presetcount(_soundfont)); return; } } } bool midiIs(const void *bytes, size_t size) { return (size > MIDI_HEADER_BYTES) && (memcmp(bytes, "MThd", MIDI_HEADER_BYTES) == 0); } bool midiRender(const void *bytes, size_t size, DecodedAudioT *out) { tml_message *messages = NULL; tml_message *message = NULL; float *pcm = NULL; uint32_t length = 0; int64_t total = 0; int64_t done = 0; int32_t block = 0; double played = 0.0; double perBlock = (double)MIDI_BLOCK_SAMPLES * MS_PER_SECOND / MIDI_SAMPLE_RATE; memset(out, 0, sizeof(*out)); if (_soundfont == NULL) { SDL_SetError("This is a MIDI file, and no SoundFont was found to play it with. Name one with --soundfont."); return false; } if (size > INT32_MAX) { SDL_SetError("The MIDI file is too large."); return false; } messages = tml_load_memory(bytes, (int32_t)size); if (messages == NULL) { SDL_SetError("The MIDI file would not parse."); return false; } tml_get_info(messages, NULL, NULL, NULL, NULL, &length); if ((length + MIDI_TAIL_MS) > MIDI_MAX_MS) { length = MIDI_MAX_MS - MIDI_TAIL_MS; } // Whole blocks, so the render loop never has a partial one to think about. total = (((int64_t)length + MIDI_TAIL_MS) * MIDI_SAMPLE_RATE / MS_PER_SECOND + MIDI_BLOCK_SAMPLES - 1) / MIDI_BLOCK_SAMPLES * MIDI_BLOCK_SAMPLES; pcm = (float *)SDL_malloc((size_t)total * MIDI_CHANNELS * sizeof(float)); if (pcm == NULL) { SDL_SetError("Out of memory rendering a MIDI file."); tml_free(messages); return false; } tsf_reset(_soundfont); message = messages; while (done < total) { block = (int32_t)((total - done > MIDI_BLOCK_SAMPLES) ? MIDI_BLOCK_SAMPLES : (total - done)); played += perBlock; while ((message != NULL) && (message->time <= (uint32_t)played)) { _apply(message); message = message->next; } tsf_render_float(_soundfont, pcm + done * MIDI_CHANNELS, block, 0); done += block; } tml_free(messages); out->pcm = (uint8_t *)pcm; out->bytes = (size_t)total * MIDI_CHANNELS * sizeof(float); out->spec.format = SDL_AUDIO_F32; out->spec.channels = MIDI_CHANNELS; out->spec.freq = MIDI_SAMPLE_RATE; return true; } void midiQuit(void) { if (_soundfont != NULL) { tsf_close(_soundfont); _soundfont = NULL; } free(_found); _found = NULL; } const char *midiSoundfont(void) { return (_found != NULL) ? _found : "none found (MIDI will not play; name one with --soundfont)"; }