Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

native_midi: Add support for native MIDI on Linux #595

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

tatokis
Copy link
Contributor

@tatokis tatokis commented Feb 23, 2024

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:

  • SDL_NATIVE_MUSIC_ALLOW_PAUSE
  • SDL_NATIVE_MUSIC_NO_CONNECT_PORTS

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
Copy link
Contributor

sezero commented Feb 23, 2024

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
Copy link
Contributor Author

tatokis commented Feb 23, 2024

@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.

Comment on lines +999 to +998
elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
list(APPEND SDL3MIXER_MIDI_NATIVE_ENABLED TRUE)
target_sources(${sdl3_mixer_target_name} PRIVATE src/codecs/native_midi/native_midi_linux_alsa.c ${midi_common_sources})
target_link_libraries(${sdl3_mixer_target_name} PRIVATE asound)
Copy link
Contributor

@madebr madebr Feb 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think linking to asound might be an issue: not everybody has alsa installed. It should also be possible to load alsa dynamically (using SDL_LoadObject).
Also, should we call alsa "native"? Isn't it "just" a library, that happens to be *nix-only, just like pipewire? Perhaps define a new SDL3MIXER_MIDI_ALSA option?

cmake_dependent_option(SDL3MIXER_MIDI_ALSA "Support for playing MIDI with alsa-lib" ON "SDL3MIXER_MIDI;UNIX;NOT APPLE" OFF)`
cmake_dependent_option(SDL3MIXER_MIDI_ALSA_SHARED "Load alsa-lib dynamically" ON "SDL3MIXER_MIDI_ALSA" OFF)`

With those addressed, I'll help with the cmake changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it "just" a library, that happens to be Linux-only, just like pipewire?

ALSA (and in this case snd_seq.*) is a kernel module/implementation. libasound is a library to make talking to the kernel easier. PipeWire is a userspace daemon on the other hand that also depends on ALSA support in the kernel (to output audio to physical ports), and runs on top of it as that is how the Linux kernel itself implements audio.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it's a "helper library", so not guaranteed to be installed on a system (forget about my remark about pipewire).

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:
- SDL_NATIVE_MUSIC_ALLOW_PAUSE
- SDL_NATIVE_MUSIC_NO_CONNECT_PORTS

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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants