libsdl-org / SDL_mixer

An audio mixer that supports various file formats for Simple Directmedia Layer.
zlib License
431 stars 147 forks source link

native_midi: Add support for native MIDI on Linux #595

Open tatokis opened 9 months ago

tatokis commented 9 months ago

This adds support for native MIDI on Linux using the ALSA sequencer API.

Playback is performed by spawning a thread which processes SMF events, converts them to ALSA SEQ ones, and forwards them to a synth client.

To ensure responsiveness (and not cause applications to freeze), the sequencer API is used in nonblock mode. When an event is added to a queue, it is first sent to a userspace buffer, which is eventually flushed to a kernel buffer and then sent to the destination. This means that events are processed in chunks until the buffer is filled which then gets drained, and not in realtime.

A socketpair is set up for the main thread to control the playback thread, which uses poll() to wait until IO can be performed on the sequencer or a command can be received from the main thread.

The playback thread reports its status by writing to an atomic enum.

Two new hints are introduced:

Pausing is implemented by stopping the queue and setting the volume to 0 with a GM SysEx Master Volume message. Since at the time of writing none of the most common use cases support it (FluidSynth, Emu10k1 WaveTable, Linux OPL3 MIDI Synth), pausing is disabled by default as it is preferred to have music playing instead of listening to hanging notes.

The hint SDL_NATIVE_MUSIC_ALLOW_PAUSE was added so that a user with a compatible MIDI device can simply set the environment variable SDL_NATIVE_MUSIC_ALLOW_PAUSE=1 to enable pausing.

The implementation outputs events to any client subscribed to its port. When the hint SDL_NATIVE_MUSIC_NO_CONNECT_PORTS is set, it does not attempt to automatically connect the port to a client. This might be desired if the end user uses an external patchbay application. For example, on a modern system with PipeWire one might want to route the application to a PW MIDI client through the bridge: qpwgraph showing odamex connected to pw-midirecord

Otherwise, it first checks for the environment variable ALSA_OUTPUT_PORTS, and if successfully parsed and the client is found, the output port is connected to it automatically. This env var is used by aplaymidi. If it could not be parsed, or it is not set, then the first MIDI synth client is preferred. If one is not found, then the application connects its port to any available client as a last resort.

This is the SDL3 port of my patch, and has only been tested against playmus, as I know of no SDL3 applications that support MIDI playback, and the SDL2 ones that do, still load the SDL2 version of SDL_Mixer when ran with sdl2-compat.

The SDL2 version can be found at native-midi-linux, and once this is reviewed and hopefully merged, I'd like to get that one in for SDL2_Mixer due to the above, although I understand if you do not want to introduce new code to it.

sezero commented 9 months ago

I guess alsa-lib-devel needs to be present for this (version min required?) and that needs checking in cmake'ry and condition added to new source. (@madebr may help with that if this is accepted.)

tatokis commented 9 months ago

@sezero

I guess alsa-lib-devel needs to be present for this (version min required?)

Indeed. I have no idea what the minimum version is, as if I recall correctly none of the snd_.+ functions I used specify a minimum version in the documentation. I'll check and get back to you.

that needs checking in cmake'ry and condition added to new source

I assumed that a Linux system will always come with headers for libasound, at least in the context of SDL_Mixer, but you are correct. I'll add a proper check in cmake.