From 2418042eb83564f20d4492c1f53c97682c86a0ae Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Wed, 18 Sep 2024 18:32:19 -0400 Subject: [PATCH 1/9] native_midi: Initial stubs for ALSA module This does not do anything yet, but does set up the necessary build changes so that we will link against the libasound library. --- CMakeLists.txt | 11 +++- src/codecs/native_midi/native_midi_alsa.c | 76 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/codecs/native_midi/native_midi_alsa.c diff --git a/CMakeLists.txt b/CMakeLists.txt index ee191c37e..cec148153 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,7 +140,13 @@ option(SDL2MIXER_MIDI "Enable MIDI music" ON) cmake_dependent_option(SDL2MIXER_MIDI_FLUIDSYNTH "Support FluidSynth MIDI output" ON "SDL2MIXER_MIDI;NOT SDL2MIXER_VENDORED" OFF) cmake_dependent_option(SDL2MIXER_MIDI_FLUIDSYNTH_SHARED "Dynamically load libfluidsynth" "${SDL2MIXER_DEPS_SHARED}" SDL2MIXER_MIDI_FLUIDSYNTH OFF) -if(WIN32 OR APPLE OR HAIKU) +if ("${CMAKE_SYSTEM}" MATCHES "Linux") + set(LINUX ON) +else() + set(LINUX OFF) +endif() + +if(WIN32 OR APPLE OR HAIKU OR LINUX) cmake_dependent_option(SDL2MIXER_MIDI_NATIVE "Support native MIDI output" ON SDL2MIXER_MIDI OFF) else() set(SDL2MIXER_MIDI_NATIVE OFF) @@ -805,6 +811,9 @@ if(SDL2MIXER_MIDI_NATIVE) if(WIN32) target_sources(SDL2_mixer PRIVATE src/codecs/native_midi/native_midi_win32.c) target_link_libraries(SDL2_mixer PRIVATE winmm) + elseif ("${CMAKE_SYSTEM}" MATCHES "Linux") + target_sources(SDL2_mixer PRIVATE src/codecs/native_midi/native_midi_alsa.c) + target_link_libraries(SDL2_mixer PRIVATE asound) elseif(APPLE) target_sources(SDL2_mixer PRIVATE src/codecs/native_midi/native_midi_macosx.c) target_link_libraries(SDL2_mixer PRIVATE -Wl,-framework,AudioToolbox -Wl,-framework,AudioUnit -Wl,-framework,CoreServices) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c new file mode 100644 index 000000000..70c25ec97 --- /dev/null +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -0,0 +1,76 @@ +/* + native_midi: Linux (ALSA) native MIDI for the SDL_mixer library + Copyright (C) 2024 Simon Howard + + 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. +*/ +#include "SDL_config.h" +#ifdef __LINUX__ + +#include "native_midi.h" +#include "native_midi_common.h" + +struct _NativeMidiSong { +}; + +static NativeMidiSong *currentsong; + +int native_midi_detect(void) +{ + return 0; +} + +NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *src, int freesrc) +{ + return NULL; +} + +void native_midi_freesong(NativeMidiSong *song) +{ +} + +void native_midi_start(NativeMidiSong *song, int loops) +{ +} + +void native_midi_pause(void) +{ +} + +void native_midi_resume(void) +{ +} + +void native_midi_stop(void) +{ +} + +int native_midi_active(void) +{ + return 0; +} + +void native_midi_setvolume(int volume) +{ +} + +const char *native_midi_error(void) +{ + return ""; +} + +#endif /* #ifdef __LINUX__ */ From cd39fbe5edf49378a40dbd6579d8cf8e6a52d1df Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Wed, 18 Sep 2024 18:33:27 -0400 Subject: [PATCH 2/9] native_midi: Initial loading from MIDI file --- src/codecs/native_midi/native_midi_alsa.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index 70c25ec97..e5d36465f 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -25,6 +25,8 @@ #include "native_midi_common.h" struct _NativeMidiSong { + Uint16 division; + MIDIEvent *event_list; }; static NativeMidiSong *currentsong; @@ -36,11 +38,24 @@ int native_midi_detect(void) NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *src, int freesrc) { - return NULL; + NativeMidiSong *result = SDL_malloc(sizeof(NativeMidiSong)); + if (result == NULL) { + return NULL; + } + + result->event_list = CreateMIDIEventList(src, &result->division); + if (result->event_list == NULL) { + SDL_free(result); + return NULL; + } + + return result; } void native_midi_freesong(NativeMidiSong *song) { + FreeMIDIEventList(song->event_list); + SDL_free(song); } void native_midi_start(NativeMidiSong *song, int loops) From 78409577639cb0a81df77134fc6ce15a17f10e45 Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Tue, 17 Sep 2024 18:47:29 -0400 Subject: [PATCH 3/9] native_midi: Open ALSA device, start thread We don't write any MIDI data to the sequencer yet but this sets up the basic foundations. For now, the playback thread just exits immediately, so `playmus` completes successfully. --- src/codecs/native_midi/native_midi_alsa.c | 64 +++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index e5d36465f..e5962c833 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -21,6 +21,8 @@ #include "SDL_config.h" #ifdef __LINUX__ +#include + #include "native_midi.h" #include "native_midi_common.h" @@ -29,11 +31,37 @@ struct _NativeMidiSong { MIDIEvent *event_list; }; -static NativeMidiSong *currentsong; +static enum { STOPPED, PLAYING, SHUTDOWN } state = STOPPED; +static SDL_Thread *native_midi_thread; +static snd_seq_t *output; +static int output_queue; int native_midi_detect(void) { - return 0; + int err; + + if (output != NULL) { + return 1; + } + + // TODO: Allow output port to be specified explicitly + err = snd_seq_open(&output, "default", SND_SEQ_OPEN_OUTPUT, 0); + if (err < 0) { + SDL_Log("native_midi_detect: Failed to open sequencer device: %s", + snd_strerror(err)); + return 0; + } + snd_seq_set_client_name(output, "SDL_mixer"); + snd_seq_set_output_buffer_size(output, 512); + + output_queue = snd_seq_alloc_queue(output); + if (output_queue < 0) { + snd_seq_close(output); + output = NULL; + return 0; + } + + return 1; } NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *src, int freesrc) @@ -58,25 +86,55 @@ void native_midi_freesong(NativeMidiSong *song) SDL_free(song); } +static int playback_thread(void *data) +{ + NativeMidiSong *song = data; + + snd_seq_start_queue(output, output_queue, NULL); + + while (state == PLAYING) { + // TODO + break; + } + + state = STOPPED; + return 0; +} + void native_midi_start(NativeMidiSong *song, int loops) { + native_midi_stop(); + state = PLAYING; + native_midi_thread = SDL_CreateThread( + playback_thread, "native midi playback", song); } void native_midi_pause(void) { + snd_seq_stop_queue(output, output_queue, NULL); } void native_midi_resume(void) { + snd_seq_continue_queue(output, output_queue, NULL); } void native_midi_stop(void) { + if (state != PLAYING) { + return; + } + + state = SHUTDOWN; + SDL_WaitThread(native_midi_thread, NULL); + + snd_seq_drop_output(output); + snd_seq_stop_queue(output, output_queue, NULL); } int native_midi_active(void) { - return 0; + return state == PLAYING; } void native_midi_setvolume(int volume) From 0aaa89da6c349893b1cd83988c118cd9a60de66c Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Fri, 20 Sep 2024 18:28:03 -0400 Subject: [PATCH 4/9] native_midi: Connect to ALSA destination port We try various different ports that are usually the "default"; we need to provide a mechanism to specify it explicitly but for now this will do. These are the same "default" ports that DOSbox's ALSA code tries. --- src/codecs/native_midi/native_midi_alsa.c | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index e5962c833..71bccc247 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -26,6 +26,12 @@ #include "native_midi.h" #include "native_midi_common.h" +static const snd_seq_addr_t default_ports[] = { + {65, 0}, + {17, 0}, + {128, 0}, // Usual port for timidity +}; + struct _NativeMidiSong { Uint16 division; MIDIEvent *event_list; @@ -33,9 +39,35 @@ struct _NativeMidiSong { static enum { STOPPED, PLAYING, SHUTDOWN } state = STOPPED; static SDL_Thread *native_midi_thread; +static snd_seq_addr_t connected_addr; static snd_seq_t *output; +static int local_port; static int output_queue; +static SDL_bool try_connect(void) +{ + int i; + + local_port = snd_seq_create_simple_port(output, "SDL_mixer", + SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC); + if (local_port < 0) { + return SDL_FALSE; + } + + for (i = 0; i < sizeof(default_ports) / sizeof(*default_ports); ++i) { + if (snd_seq_connect_to(output, local_port, default_ports[i].client, + default_ports[i].port) == 0) { + connected_addr = default_ports[i]; + return SDL_TRUE; + } + } + + SDL_Log("native_midi_detect: Failed to find an output sequencer device."); + + return SDL_FALSE; +} + int native_midi_detect(void) { int err; @@ -54,6 +86,12 @@ int native_midi_detect(void) snd_seq_set_client_name(output, "SDL_mixer"); snd_seq_set_output_buffer_size(output, 512); + if (!try_connect()) { + snd_seq_close(output); + output = NULL; + return 0; + } + output_queue = snd_seq_alloc_queue(output); if (output_queue < 0) { snd_seq_close(output); @@ -61,6 +99,9 @@ int native_midi_detect(void) return 0; } + SDL_Log("native_midi_detect: Opened ALSA sequencer port %d:%d", + connected_addr.client, connected_addr.port); + return 1; } From 1d627cd1d2e4927c6a044d788e66078b1a7441ba Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Fri, 20 Sep 2024 18:52:19 -0400 Subject: [PATCH 5/9] native_midi: Initial conversion of MIDI events This converts all the common MIDI events to the ALSA event types. The converted events are not yet sent to ALSA. --- src/codecs/native_midi/native_midi_alsa.c | 83 ++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index 71bccc247..f8755fe7d 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -127,15 +127,94 @@ void native_midi_freesong(NativeMidiSong *song) SDL_free(song); } +static int map_event_type(int ev_type) +{ + switch (ev_type) { + case MIDI_STATUS_NOTE_OFF: + return SND_SEQ_EVENT_NOTEOFF; + case MIDI_STATUS_NOTE_ON: + return SND_SEQ_EVENT_NOTEON; + case MIDI_STATUS_AFTERTOUCH: + return SND_SEQ_EVENT_KEYPRESS; + case MIDI_STATUS_CONTROLLER: + return SND_SEQ_EVENT_CONTROLLER; + case MIDI_STATUS_PROG_CHANGE: + return SND_SEQ_EVENT_PGMCHANGE; + case MIDI_STATUS_PRESSURE: + return SND_SEQ_EVENT_CHANPRESS; + case MIDI_STATUS_PITCH_WHEEL: + return SND_SEQ_EVENT_PITCHBEND; + case MIDI_STATUS_SYSEX: + return SND_SEQ_EVENT_SYSEX; + default: + return SND_SEQ_EVENT_NONE; + } +} + +static void convert_event(snd_seq_event_t *alsa_ev, MIDIEvent *ev) +{ + switch ((ev->status & 0xf0) >> 4) { + case MIDI_STATUS_NOTE_OFF: + case MIDI_STATUS_NOTE_ON: + case MIDI_STATUS_AFTERTOUCH: + snd_seq_ev_set_fixed(alsa_ev); + alsa_ev->data.note.channel = ev->status & 0x0f; + alsa_ev->data.note.note = ev->data[0]; + alsa_ev->data.note.velocity = ev->data[1]; + break; + + case MIDI_STATUS_CONTROLLER: + snd_seq_ev_set_fixed(alsa_ev); + alsa_ev->data.control.channel = ev->status & 0x0f; + alsa_ev->data.control.param = ev->data[0]; + alsa_ev->data.control.value = ev->data[1]; + break; + + case MIDI_STATUS_PROG_CHANGE: + case MIDI_STATUS_PRESSURE: + snd_seq_ev_set_fixed(alsa_ev); + alsa_ev->data.control.channel = ev->status & 0x0f; + alsa_ev->data.control.value = ev->data[0]; + break; + + case MIDI_STATUS_PITCH_WHEEL: + snd_seq_ev_set_fixed(alsa_ev); + alsa_ev->data.control.channel = ev->status & 0x0f; + alsa_ev->data.control.value = + ((ev->data[0]) | ((ev->data[1]) << 7)) - 0x2000; + break; + + case MIDI_STATUS_SYSEX: + snd_seq_ev_set_variable(alsa_ev, ev->extraLen, ev->extraData); + break; + + default: + break; + } +} + static int playback_thread(void *data) { NativeMidiSong *song = data; + MIDIEvent *ev = NULL; + snd_seq_event_t alsa_ev; snd_seq_start_queue(output, output_queue, NULL); while (state == PLAYING) { - // TODO - break; + if (ev == NULL) { + ev = song->event_list; + if (ev == NULL) { + break; + } + } + + snd_seq_ev_clear(&alsa_ev); + alsa_ev.type = map_event_type((ev->status & 0xf0) >> 4); + snd_seq_ev_set_source(&alsa_ev, local_port); + snd_seq_ev_set_subs(&alsa_ev); + snd_seq_ev_schedule_tick(&alsa_ev, output_queue, 0, ev->time); + convert_event(&alsa_ev, ev); } state = STOPPED; From 85bc9bdaaa17592e07ef95604be8043b00fa8a95 Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Thu, 19 Sep 2024 23:06:44 -0400 Subject: [PATCH 6/9] native_midi: Send events to destination With this change in place the module actually works! There's more still to finish but this is actually usable. --- src/codecs/native_midi/native_midi_alsa.c | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index f8755fe7d..ccea12933 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -193,12 +193,28 @@ static void convert_event(snd_seq_event_t *alsa_ev, MIDIEvent *ev) } } +static void set_queue_tempo(Uint16 division) +{ + int err; + + // TODO: SMPTE + snd_seq_queue_tempo_t *queue_tempo; + snd_seq_queue_tempo_alloca(&queue_tempo); + snd_seq_queue_tempo_set_tempo(queue_tempo, 500000); + snd_seq_queue_tempo_set_ppq(queue_tempo, division); + err = snd_seq_set_queue_tempo(output, output_queue, queue_tempo); + if (err < 0) { + SDL_Log("Failed to set tempo: err=%d", err); + } +} + static int playback_thread(void *data) { NativeMidiSong *song = data; MIDIEvent *ev = NULL; snd_seq_event_t alsa_ev; + set_queue_tempo(song->division); snd_seq_start_queue(output, output_queue, NULL); while (state == PLAYING) { @@ -215,6 +231,14 @@ static int playback_thread(void *data) snd_seq_ev_set_subs(&alsa_ev); snd_seq_ev_schedule_tick(&alsa_ev, output_queue, 0, ev->time); convert_event(&alsa_ev, ev); + ev = ev->next; + + snd_seq_event_output(output, &alsa_ev); + + // TODO: Looping + if (ev == NULL) { + break; + } } state = STOPPED; From 5c1030894d9d06a381e7fe28b78f3280d59a7024 Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Fri, 20 Sep 2024 18:47:48 -0400 Subject: [PATCH 7/9] native_midi: Implement looping --- src/codecs/native_midi/native_midi_alsa.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index ccea12933..d2e33dd99 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -43,6 +43,7 @@ static snd_seq_addr_t connected_addr; static snd_seq_t *output; static int local_port; static int output_queue; +static int plays_remaining; // -1 means "loop forever" static SDL_bool try_connect(void) { @@ -213,12 +214,21 @@ static int playback_thread(void *data) NativeMidiSong *song = data; MIDIEvent *ev = NULL; snd_seq_event_t alsa_ev; + int last_event_time = 0, time_offset = 0; + snd_seq_drop_output(output); set_queue_tempo(song->division); snd_seq_start_queue(output, output_queue, NULL); while (state == PLAYING) { if (ev == NULL) { + // Loop until plays_remaining is zero, then we stop. + if (plays_remaining == 0) { + break; + } else if (plays_remaining > 0) { + --plays_remaining; + } + time_offset = last_event_time + 100; ev = song->event_list; if (ev == NULL) { break; @@ -229,19 +239,19 @@ static int playback_thread(void *data) alsa_ev.type = map_event_type((ev->status & 0xf0) >> 4); snd_seq_ev_set_source(&alsa_ev, local_port); snd_seq_ev_set_subs(&alsa_ev); - snd_seq_ev_schedule_tick(&alsa_ev, output_queue, 0, ev->time); + + snd_seq_ev_schedule_tick(&alsa_ev, output_queue, 0, + time_offset + ev->time); + last_event_time = time_offset + ev->time; + convert_event(&alsa_ev, ev); ev = ev->next; snd_seq_event_output(output, &alsa_ev); - - // TODO: Looping - if (ev == NULL) { - break; - } } state = STOPPED; + snd_seq_drain_output(output); return 0; } @@ -249,6 +259,7 @@ void native_midi_start(NativeMidiSong *song, int loops) { native_midi_stop(); state = PLAYING; + plays_remaining = loops < 0 ? -1 : loops + 1; native_midi_thread = SDL_CreateThread( playback_thread, "native midi playback", song); } From fe25166c80b7455b2cbdaec5199ac8e15585044d Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Mon, 23 Sep 2024 21:07:58 -0400 Subject: [PATCH 8/9] native_midi: Send reset messages on track stop The ALSA device we're sending messages to will continue playing them even if the program quits, and we don't want to leave notes hanging. So when stopping the current track, send the "notes all off" and "reset all controllers" messages to every channel, and also send an ALSA reset event as well for good measure (the Timidity++ daemon seems to respond to it) --- src/codecs/native_midi/native_midi_alsa.c | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index d2e33dd99..5e3b4aa6a 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -209,6 +209,34 @@ static void set_queue_tempo(Uint16 division) } } +static void send_reset(void) +{ + static snd_seq_event_t alsa_ev; + int i; + + snd_seq_ev_clear(&alsa_ev); + snd_seq_ev_set_source(&alsa_ev, local_port); + snd_seq_ev_set_subs(&alsa_ev); + snd_seq_ev_schedule_tick(&alsa_ev, output_queue, 0, 0); + + // We send an ALSA reset event, but first send the standard MIDI control + // events to stop all notes on all channels, just in case. + for (i = 0; i < 16; i++) { + alsa_ev.type = SND_SEQ_EVENT_CONTROLLER; + snd_seq_ev_set_fixed(&alsa_ev); + alsa_ev.data.control.channel = i; + alsa_ev.data.control.param = MIDI_CTL_ALL_NOTES_OFF; + alsa_ev.data.control.value = 0; + snd_seq_event_output(output, &alsa_ev); + + alsa_ev.data.control.param = MIDI_CTL_RESET_CONTROLLERS; + snd_seq_event_output(output, &alsa_ev); + } + + alsa_ev.type = SND_SEQ_EVENT_RESET; + snd_seq_event_output(output, &alsa_ev); +} + static int playback_thread(void *data) { NativeMidiSong *song = data; @@ -219,6 +247,7 @@ static int playback_thread(void *data) snd_seq_drop_output(output); set_queue_tempo(song->division); snd_seq_start_queue(output, output_queue, NULL); + send_reset(); while (state == PLAYING) { if (ev == NULL) { @@ -284,6 +313,8 @@ void native_midi_stop(void) SDL_WaitThread(native_midi_thread, NULL); snd_seq_drop_output(output); + send_reset(); + snd_seq_drain_output(output); snd_seq_stop_queue(output, output_queue, NULL); } From 274d4f6257f170047f94ec7d2f9a694920b67310 Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Tue, 24 Sep 2024 00:03:57 -0400 Subject: [PATCH 9/9] native_midi: Use non-blocking mode and polling When stopping a song we must wait for the playback thread to terminate. However, the thread may currently be doing a blocking write of some MIDI events and we don't actually know how long this may take to complete. Instead, we can use non-blocking mode and the poll() system call. This allows us to sleep when the output event buffer is full, but also allows us to wake it back up when it's time for the song to stop. We accomplish this by creating a pipe that we close on shutdown. --- src/codecs/native_midi/native_midi_alsa.c | 45 +++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/codecs/native_midi/native_midi_alsa.c b/src/codecs/native_midi/native_midi_alsa.c index 5e3b4aa6a..9a455a396 100644 --- a/src/codecs/native_midi/native_midi_alsa.c +++ b/src/codecs/native_midi/native_midi_alsa.c @@ -44,6 +44,7 @@ static snd_seq_t *output; static int local_port; static int output_queue; static int plays_remaining; // -1 means "loop forever" +static int poll_abort_pipe[2]; static SDL_bool try_connect(void) { @@ -78,14 +79,14 @@ int native_midi_detect(void) } // TODO: Allow output port to be specified explicitly - err = snd_seq_open(&output, "default", SND_SEQ_OPEN_OUTPUT, 0); + err = snd_seq_open(&output, "default", SND_SEQ_OPEN_OUTPUT, + SND_SEQ_NONBLOCK); if (err < 0) { SDL_Log("native_midi_detect: Failed to open sequencer device: %s", snd_strerror(err)); return 0; } snd_seq_set_client_name(output, "SDL_mixer"); - snd_seq_set_output_buffer_size(output, 512); if (!try_connect()) { snd_seq_close(output); @@ -237,6 +238,23 @@ static void send_reset(void) snd_seq_event_output(output, &alsa_ev); } +static void poll_output(void) +{ + struct pollfd fds[2]; + + // Block until more events can (potentially) be written to the + // ALSA output stream. + snd_seq_poll_descriptors(output, &fds[0], 1, POLLOUT); + + // We also block on one of the file descriptors from the abort pipe; + // this allows native_midi_stop() below to trigger poll() to return + // and the playback thread to terminate. + fds[1].fd = poll_abort_pipe[0]; + fds[1].events = POLLHUP|POLLERR; + + poll(fds, 2, -1); +} + static int playback_thread(void *data) { NativeMidiSong *song = data; @@ -276,17 +294,32 @@ static int playback_thread(void *data) convert_event(&alsa_ev, ev); ev = ev->next; - snd_seq_event_output(output, &alsa_ev); + // We use nonblocking mode, so we may not be able to write the + // event to the buffer yet. If so, we poll until we can. + while (state == PLAYING) { + snd_seq_drain_output(output); + if (snd_seq_event_output_buffer(output, &alsa_ev) != -EAGAIN) { + break; + } + poll_output(); + } } state = STOPPED; snd_seq_drain_output(output); + close(poll_abort_pipe[0]); + close(poll_abort_pipe[1]); + return 0; } void native_midi_start(NativeMidiSong *song, int loops) { native_midi_stop(); + if (pipe(poll_abort_pipe) != 0) { + SDL_Log("Failed to create poll abort pipe: %s", strerror(errno)); + return; + } state = PLAYING; plays_remaining = loops < 0 ? -1 : loops + 1; native_midi_thread = SDL_CreateThread( @@ -309,7 +342,13 @@ void native_midi_stop(void) return; } + // We trigger shutdown of the native MIDI thread by closing the file + // descriptors for the abort pipe. This causes the poll_output() + // function above to return instead of blocking on output, and the + // playback thread to terminate. state = SHUTDOWN; + close(poll_abort_pipe[0]); + close(poll_abort_pipe[1]); SDL_WaitThread(native_midi_thread, NULL); snd_seq_drop_output(output);