From 658debdcf30376cfa1ac3432d2b220824292ce3e Mon Sep 17 00:00:00 2001 From: kichikuou Date: Tue, 22 Sep 2026 16:00:21 +0900 Subject: [PATCH] SDL: Add SDL3 MIDI backend Play MIDI through the shared SDL3_mixer music track while preserving the existing loop, pause, volume, position, and fade behavior. Support FluidSynth SoundFonts and Timidity selection through their established environment variables. --- src/CMakeLists.txt | 6 +- src/midi.sdl3mixer.c | 247 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/midi.sdl3mixer.c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 25ab5dc..022613b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -128,7 +128,11 @@ if (EMSCRIPTEN) elseif (ANDROID) target_sources(xsystem35 PRIVATE midi.android.c) else() - target_sources(xsystem35 PRIVATE midi.sdlmixer.c) + if (XSYSTEM35_SDL_VERSION STREQUAL "3") + target_sources(xsystem35 PRIVATE midi.sdl3mixer.c) + else() + target_sources(xsystem35 PRIVATE midi.sdlmixer.c) + endif() if (ENABLE_MIDI_PORTMIDI) target_sources(xsystem35 PRIVATE midi.portmidi.c midifile.c) endif() diff --git a/src/midi.sdl3mixer.c b/src/midi.sdl3mixer.c new file mode 100644 index 0000000..9d39317 --- /dev/null +++ b/src/midi.sdl3mixer.c @@ -0,0 +1,247 @@ +/* + * midi.sdl3mixer.c MIDI playback using SDL3_mixer + * + * Copyright (C) 2019 + * + * 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 2 of the License, or + * (at your option) any later version. + */ + +#include +#include + +#include "midi.h" +#include "music_private.h" +#include "sdl3_mixer_backend.h" +#include "system.h" + +#define FLUIDSYNTH_SOUNDFONT_PATH \ + "SDL_mixer.decoder.fluidsynth.soundfont_path" + +static uint32_t start_time; +static uint32_t fade_tick; +static int current_vol = 100; + +static bool has_decoder(const char *name) +{ + for (int i = 0; i < MIX_GetNumAudioDecoders(); i++) { + if (!strcmp(MIX_GetAudioDecoder(i), name)) + return true; + } + return false; +} + +static int clamp_volume(int volume) +{ + if (volume < 0) + return 0; + if (volume > 100) + return 100; + return volume; +} + +static float music_gain(void) +{ + float gain = clamp_volume(current_vol) / 100.0f; + return gain * clamp_volume(prv.volval[BGM_VOLVAL_CH]) / 100.0f; +} + +static void apply_volume(void) +{ + sdl3_mixer_set_music_gain(SDL3_MIXER_MUSIC_MIDI, music_gain()); +} + +static char *last_soundfont_path(const char *paths) +{ + const char *last = NULL; + size_t last_length = 0; + const char *segment = paths; + for (const char *p = paths;; p++) { + if (*p != ';' && *p) + continue; + if (p != segment) { + last = segment; + last_length = p - segment; + } + if (!*p) + break; + segment = p + 1; + } + return last ? SDL_strndup(last, last_length) : NULL; +} + +static MIX_Audio *load_midi(const uint8_t *data, int length) +{ + if (length <= 0) + return NULL; + SDL_IOStream *io = SDL_IOFromConstMem(data, length); + SDL_PropertiesID props = SDL_CreateProperties(); + if (!io || !props) { + if (io) + SDL_CloseIO(io); + if (props) + SDL_DestroyProperties(props); + return NULL; + } + + bool success = + SDL_SetPointerProperty(props, + MIX_PROP_AUDIO_LOAD_IOSTREAM_POINTER, io) && + SDL_SetBooleanProperty(props, + MIX_PROP_AUDIO_LOAD_CLOSEIO_BOOLEAN, true) && + SDL_SetPointerProperty(props, + MIX_PROP_AUDIO_LOAD_PREFERRED_MIXER_POINTER, + sdl3_mixer_backend_get()); + + const char *soundfonts = getenv("SDL_SOUNDFONTS"); + const char *timidity_cfg = getenv("TIMIDITY_CFG"); + char *soundfont_path = NULL; + if (success && soundfonts && *soundfonts && has_decoder("FLUIDSYNTH")) { + soundfont_path = last_soundfont_path(soundfonts); + success = soundfont_path && + SDL_SetStringProperty(props, MIX_PROP_AUDIO_DECODER_STRING, + "FLUIDSYNTH") && + SDL_SetStringProperty(props, FLUIDSYNTH_SOUNDFONT_PATH, + soundfont_path); + } else if (success && timidity_cfg && *timidity_cfg && + has_decoder("TIMIDITY")) { + success = SDL_SetStringProperty(props, MIX_PROP_AUDIO_DECODER_STRING, + "TIMIDITY"); + } + + MIX_Audio *audio = success ? MIX_LoadAudioWithProperties(props) : NULL; + if (!success) + SDL_CloseIO(io); + SDL_free(soundfont_path); + SDL_DestroyProperties(props); + return audio; +} + +static bool midi_initialize(int subdevice) +{ + (void)subdevice; + return sdl3_mixer_backend_get() && + (has_decoder("FLUIDSYNTH") || has_decoder("TIMIDITY")); +} + +static void midi_stop(void) +{ + sdl3_mixer_stop_music(SDL3_MIXER_MUSIC_MIDI, 0); +} + +static void midi_exit(void) +{ + midi_stop(); +} + +static void midi_reset(void) +{ + midi_stop(); +} + +static bool midi_start(int no, int loop, const uint8_t *data, int length) +{ + (void)no; + midi_stop(); + MIX_Audio *audio = load_midi(data, length); + if (!audio) { + WARNING("Cannot load MIDI: %s", SDL_GetError()); + return false; + } + + /* MIDI passes loop directly; zero means infinite playback. */ + int loops = loop == 0 ? -1 : loop; + if (!sdl3_mixer_play_music(SDL3_MIXER_MUSIC_MIDI, audio, + loops, 0, music_gain())) { + WARNING("Cannot play MIDI: %s", SDL_GetError()); + return false; + } + start_time = sys_get_ticks(); + return true; +} + +static void midi_pause(void) +{ + sdl3_mixer_pause_music(SDL3_MIXER_MUSIC_MIDI); +} + +static void midi_unpause(void) +{ + sdl3_mixer_resume_music(SDL3_MIXER_MUSIC_MIDI); +} + +static bool midi_get_playing_info(midiplaystate *state) +{ + if (!sdl3_mixer_music_active(SDL3_MIXER_MUSIC_MIDI)) { + state->in_play = false; + state->loc_ms = 0; + return true; + } + state->in_play = true; + state->loc_ms = sys_get_ticks() - start_time; + return true; +} + +static int midi_getflag(int mode, int index) +{ + (void)mode; + (void)index; + return 0; +} + +static bool midi_setflag(int mode, int index, int value) +{ + (void)mode; + (void)index; + (void)value; + return false; +} + +static bool midi_fadestart(int time, int volume, int stop) +{ + if (time == 0) { + current_vol = volume; + apply_volume(); + if (stop) + midi_stop(); + return true; + } + + if (volume == 0) { + /* Match SDL2_mixer: a fade to silence always stops the music. */ + sdl3_mixer_stop_music(SDL3_MIXER_MUSIC_MIDI, time); + fade_tick = sys_get_ticks() + time; + return true; + } + WARNING("(time=%d, volume=%d, stop=%d) unsupported", + time, volume, stop); + return false; +} + +static bool midi_fading(void) +{ + return sys_get_ticks() < fade_tick; +} + +static void midi_reapply_volume(void) +{ + apply_volume(); +} + +mididevice_t midi_sdlmixer = { + .init = midi_initialize, + .exit = midi_exit, + .reset = midi_reset, + .start = midi_start, + .stop = midi_stop, + .pause = midi_pause, + .unpause = midi_unpause, + .getpos = midi_get_playing_info, + .getflag = midi_getflag, + .setflag = midi_setflag, + .fadestart = midi_fadestart, + .fading = midi_fading, + .reapply_volume = midi_reapply_volume, +};