Skip to content

Commit

Permalink
native_midi: Use non-blocking mode and polling
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
fragglet committed Sep 24, 2024
1 parent fe25166 commit 274d4f6
Showing 1 changed file with 42 additions and 3 deletions.
45 changes: 42 additions & 3 deletions src/codecs/native_midi/native_midi_alsa.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand Down

0 comments on commit 274d4f6

Please sign in to comment.