SDL_mixer: native_midi_linux_alsa: Implement dynamic loading of libasound

From 814476fdab337d3dc9ba76ad1a7db0bf010f4454 Mon Sep 17 00:00:00 2001
From: Tasos Sahanidis <[EMAIL REDACTED]>
Date: Fri, 1 Mar 2024 10:16:47 +0200
Subject: [PATCH] native_midi_linux_alsa: Implement dynamic loading of
 libasound

---
 .../native_midi/native_midi_linux_alsa.c      | 237 ++++++++++++++----
 1 file changed, 191 insertions(+), 46 deletions(-)

diff --git a/src/codecs/native_midi/native_midi_linux_alsa.c b/src/codecs/native_midi/native_midi_linux_alsa.c
index 2beb8cb9..a163f231 100644
--- a/src/codecs/native_midi/native_midi_linux_alsa.c
+++ b/src/codecs/native_midi/native_midi_linux_alsa.c
@@ -42,6 +42,145 @@
 #include <string.h>
 #include <sys/stat.h>
 
+//#define SDL_NATIVE_MIDI_ALSA_DYNAMIC "libasound.so.2"
+
+static int load_alsa_syms(void);
+
+#ifdef SDL_NATIVE_MIDI_ALSA_DYNAMIC
+#define snd_seq_client_info_sizeof ALSA_snd_seq_client_info_sizeof
+#define snd_seq_port_info_sizeof   ALSA_snd_seq_port_info_sizeof
+#define snd_seq_queue_tempo_sizeof ALSA_snd_seq_queue_tempo_sizeof
+#define snd_seq_control_queue      ALSA_snd_seq_control_queue
+
+static void *alsa_handle = NULL;
+
+static int load_alsa_sym(const char *fn, void **addr)
+{
+    *addr = SDL_LoadFunction(alsa_handle, fn);
+    if (!*addr) {
+        // Don't call SDL_SetError(): SDL_LoadFunction already did.
+        return 0;
+    }
+
+    return 1;
+}
+
+// cast funcs to char* first, to please GCC's strict aliasing rules.
+#define SDL_ALSA_SYM(x)                                 \
+    if (!load_alsa_sym(#x, (void **)(char *)&ALSA_##x)) \
+    return -1
+
+static void unload_alsa_library(void)
+{
+    if (alsa_handle) {
+        SDL_UnloadObject(alsa_handle);
+        alsa_handle = NULL;
+    }
+}
+
+static int load_alsa_library(void)
+{
+    int retval = 0;
+    if (!alsa_handle) {
+        alsa_handle = SDL_LoadObject(SDL_NATIVE_MIDI_ALSA_DYNAMIC);
+        if (!alsa_handle) {
+            retval = -1;
+            // Don't call SDL_SetError(): SDL_LoadObject already did.
+        } else {
+            retval = load_alsa_syms();
+            if (retval < 0) {
+                unload_alsa_library();
+            }
+        }
+    }
+    return retval;
+}
+
+#else
+
+#define SDL_ALSA_SYM(x) ALSA_##x = x
+static void unload_alsa_library(void)
+{
+}
+
+static int load_alsa_library(void)
+{
+    return load_alsa_syms();
+}
+#endif // SDL_NATIVE_MIDI_ALSA_DYNAMIC
+
+static int (*ALSA_snd_seq_alloc_named_queue)(snd_seq_t *seq, const char *name);
+static int (*ALSA_snd_seq_client_id)(snd_seq_t *handle);
+static int (*ALSA_snd_seq_client_info_get_client)(const snd_seq_client_info_t *info);
+static size_t (*ALSA_snd_seq_client_info_sizeof)(void);
+static int (*ALSA_snd_seq_close)(snd_seq_t *handle);
+static int (*ALSA_snd_seq_connect_to)(snd_seq_t *seq, int my_port, int dest_client, int dest_port);
+static int (*ALSA_snd_seq_control_queue)(snd_seq_t *seq, int q, int type, int value, snd_seq_event_t *ev);
+static int (*ALSA_snd_seq_create_simple_port)(snd_seq_t *seq, const char *name, unsigned int caps, unsigned int type);
+static int (*ALSA_snd_seq_delete_simple_port)(snd_seq_t *seq, int port);
+static int (*ALSA_snd_seq_drain_output)(snd_seq_t *handle);
+static int (*ALSA_snd_seq_drop_output)(snd_seq_t *handle);
+static int (*ALSA_snd_seq_event_input)(snd_seq_t *handle, snd_seq_event_t **ev);
+static int (*ALSA_snd_seq_event_output)(snd_seq_t *handle, snd_seq_event_t *ev);
+static int (*ALSA_snd_seq_event_output_direct)(snd_seq_t *handle, snd_seq_event_t *ev);
+static int (*ALSA_snd_seq_free_queue)(snd_seq_t *handle, int q);
+static int (*ALSA_snd_seq_get_any_client_info)(snd_seq_t *handle, int client, snd_seq_client_info_t *info);
+static int (*ALSA_snd_seq_get_any_port_info)(snd_seq_t *handle, int client, int port, snd_seq_port_info_t *info);
+static int (*ALSA_snd_seq_nonblock)(snd_seq_t *handle, int nonblock);
+static int (*ALSA_snd_seq_open)(snd_seq_t **handle, const char *name, int streams, int mode);
+static int (*ALSA_snd_seq_parse_address)(snd_seq_t *seq, snd_seq_addr_t *addr, const char *str);
+static int (*ALSA_snd_seq_poll_descriptors)(snd_seq_t *handle, struct pollfd *pfds, unsigned int space, short events);
+static unsigned int (*ALSA_snd_seq_port_info_get_capability)(const snd_seq_port_info_t *info);
+static int (*ALSA_snd_seq_port_info_get_port)(const snd_seq_port_info_t *info);
+static unsigned int (*ALSA_snd_seq_port_info_get_type)(const snd_seq_port_info_t *info);
+static size_t (*ALSA_snd_seq_port_info_sizeof)(void);
+static int (*ALSA_snd_seq_query_next_client)(snd_seq_t *handle, snd_seq_client_info_t *info);
+static int (*ALSA_snd_seq_query_next_port)(snd_seq_t *handle, snd_seq_port_info_t *info);
+static void (*ALSA_snd_seq_queue_tempo_set_ppq)(snd_seq_queue_tempo_t *info, int ppq);
+static void (*ALSA_snd_seq_queue_tempo_set_tempo)(snd_seq_queue_tempo_t *info, unsigned int tempo);
+static size_t (*ALSA_snd_seq_queue_tempo_sizeof)(void);
+static int (*ALSA_snd_seq_set_client_event_filter)(snd_seq_t *seq, int event_type);
+static int (*ALSA_snd_seq_set_client_name)(snd_seq_t *seq, const char *name);
+static int (*ALSA_snd_seq_set_queue_tempo)(snd_seq_t *handle, int q, snd_seq_queue_tempo_t *tempo);
+
+static int load_alsa_syms(void)
+{
+    SDL_ALSA_SYM(snd_seq_alloc_named_queue);
+    SDL_ALSA_SYM(snd_seq_client_id);
+    SDL_ALSA_SYM(snd_seq_client_info_get_client);
+    SDL_ALSA_SYM(snd_seq_client_info_sizeof);
+    SDL_ALSA_SYM(snd_seq_close);
+    SDL_ALSA_SYM(snd_seq_connect_to);
+    SDL_ALSA_SYM(snd_seq_control_queue);
+    SDL_ALSA_SYM(snd_seq_create_simple_port);
+    SDL_ALSA_SYM(snd_seq_delete_simple_port);
+    SDL_ALSA_SYM(snd_seq_drain_output);
+    SDL_ALSA_SYM(snd_seq_drop_output);
+    SDL_ALSA_SYM(snd_seq_event_input);
+    SDL_ALSA_SYM(snd_seq_event_output);
+    SDL_ALSA_SYM(snd_seq_event_output_direct);
+    SDL_ALSA_SYM(snd_seq_free_queue);
+    SDL_ALSA_SYM(snd_seq_get_any_client_info);
+    SDL_ALSA_SYM(snd_seq_get_any_port_info);
+    SDL_ALSA_SYM(snd_seq_nonblock);
+    SDL_ALSA_SYM(snd_seq_open);
+    SDL_ALSA_SYM(snd_seq_parse_address);
+    SDL_ALSA_SYM(snd_seq_poll_descriptors);
+    SDL_ALSA_SYM(snd_seq_port_info_get_capability);
+    SDL_ALSA_SYM(snd_seq_port_info_get_port);
+    SDL_ALSA_SYM(snd_seq_port_info_get_type);
+    SDL_ALSA_SYM(snd_seq_port_info_sizeof);
+    SDL_ALSA_SYM(snd_seq_query_next_client);
+    SDL_ALSA_SYM(snd_seq_query_next_port);
+    SDL_ALSA_SYM(snd_seq_queue_tempo_set_ppq);
+    SDL_ALSA_SYM(snd_seq_queue_tempo_set_tempo);
+    SDL_ALSA_SYM(snd_seq_queue_tempo_sizeof);
+    SDL_ALSA_SYM(snd_seq_set_client_event_filter);
+    SDL_ALSA_SYM(snd_seq_set_client_name);
+    SDL_ALSA_SYM(snd_seq_set_queue_tempo);
+    return 0;
+}
+
 #ifndef NDEBUG
 #define MIDIDbgLog(...) SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, __VA_ARGS__)
 #else
@@ -163,24 +302,29 @@ static snd_seq_t *open_seq(int *srcport_out)
     snd_seq_t *seq;
     int ret;
 
-    if ((ret = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0)) < 0) {
+    if (load_alsa_library()) {
+        MIDI_SET_ERROR("Failed to load libasound");
+        return NULL;
+    }
+
+    if ((ret = ALSA_snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0)) < 0) {
         MIDI_SET_ERROR("snd_seq_open returned %d", ret);
         return NULL;
     }
 
     char *seq_name = get_app_name();
     if (!seq_name) {
-        snd_seq_close(seq);
+        ALSA_snd_seq_close(seq);
         return NULL;
     }
 
-    snd_seq_set_client_name(seq, seq_name);
+    ALSA_snd_seq_set_client_name(seq, seq_name);
 
-    if ((ret = snd_seq_create_simple_port(seq, seq_name,
-                                          SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_READ | SND_SEQ_PORT_CAP_SYNC_READ,
-                                          SND_SEQ_PORT_TYPE_APPLICATION | SND_SEQ_PORT_TYPE_MIDI_GENERIC)) < 0) {
+    if ((ret = ALSA_snd_seq_create_simple_port(seq, seq_name,
+                                               SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_READ | SND_SEQ_PORT_CAP_SYNC_READ,
+                                               SND_SEQ_PORT_TYPE_APPLICATION | SND_SEQ_PORT_TYPE_MIDI_GENERIC)) < 0) {
         MIDI_SET_ERROR("snd_seq_create_simple_port failed with %d", ret);
-        snd_seq_close(seq);
+        ALSA_snd_seq_close(seq);
         SDL_free(seq_name);
         return NULL;
     }
@@ -194,8 +338,9 @@ static snd_seq_t *open_seq(int *srcport_out)
 
 static void close_seq(snd_seq_t *seq, const int port)
 {
-    snd_seq_delete_simple_port(seq, port);
-    snd_seq_close(seq);
+    ALSA_snd_seq_delete_simple_port(seq, port);
+    ALSA_snd_seq_close(seq);
+    unload_alsa_library();
 }
 
 bool native_midi_detect(void)
@@ -222,27 +367,27 @@ static SDL_INLINE int subscribe_to_first_available_port(snd_seq_t *seq, const in
     snd_seq_client_info_alloca(&clientinfo);
 
     /* Query System to fill the struct initially */
-    if (snd_seq_get_any_client_info(seq, 0, clientinfo))
+    if (ALSA_snd_seq_get_any_client_info(seq, 0, clientinfo))
         return -1;
 
-    while (snd_seq_query_next_client(seq, clientinfo) == 0) {
-        int client = snd_seq_client_info_get_client(clientinfo);
+    while (ALSA_snd_seq_query_next_client(seq, clientinfo) == 0) {
+        int client = ALSA_snd_seq_client_info_get_client(clientinfo);
 
         /* Not necessary, as we don't allow subscription to our ports, but let's ignore ourselves anyway */
-        if (client == snd_seq_client_id(seq))
+        if (client == ALSA_snd_seq_client_id(seq))
             continue;
 
         snd_seq_port_info_t *portinfo;
         snd_seq_port_info_alloca(&portinfo);
 
         /* Start with port 0 */
-        if (snd_seq_get_any_port_info(seq, client, 0, portinfo))
+        if (ALSA_snd_seq_get_any_port_info(seq, client, 0, portinfo))
             continue;
 
         do {
-            int port = snd_seq_port_info_get_port(portinfo);
-            unsigned int cap = snd_seq_port_info_get_capability(portinfo);
-            unsigned int type = snd_seq_port_info_get_type(portinfo);
+            int port = ALSA_snd_seq_port_info_get_port(portinfo);
+            unsigned int cap = ALSA_snd_seq_port_info_get_capability(portinfo);
+            unsigned int type = ALSA_snd_seq_port_info_get_type(portinfo);
 
             if ((type & required_type) == required_type &&
                 cap & SND_SEQ_PORT_CAP_WRITE &&
@@ -252,11 +397,11 @@ static SDL_INLINE int subscribe_to_first_available_port(snd_seq_t *seq, const in
                 MIDIDbgLog("Client %d Cap %x Type %x", client, cap, type);
 
                 /* Could we connect to it? */
-                if (snd_seq_connect_to(seq, srcport, client, port) == 0)
+                if (ALSA_snd_seq_connect_to(seq, srcport, client, port) == 0)
                     return 0;
             }
 
-        } while (snd_seq_query_next_port(seq, portinfo) == 0);
+        } while (ALSA_snd_seq_query_next_port(seq, portinfo) == 0);
     }
     return 1;
 }
@@ -274,8 +419,8 @@ static SDL_INLINE void pick_seq_dest_addr(NativeMidiSong *song)
     /* If ALSA_OUTPUT_PORTS is specified, try to parse it and connect to it */
     snd_seq_addr_t conn_addr;
     const char *ports_env = SDL_getenv("ALSA_OUTPUT_PORTS");
-    if (ports_env && snd_seq_parse_address(song->seq, &conn_addr, ports_env) == 0)
-        if (snd_seq_connect_to(song->seq, song->srcport, conn_addr.client, conn_addr.port) == 0)
+    if (ports_env && ALSA_snd_seq_parse_address(song->seq, &conn_addr, ports_env) == 0)
+        if (ALSA_snd_seq_connect_to(song->seq, song->srcport, conn_addr.client, conn_addr.port) == 0)
             return;
 
     /* If we're not connecting to a specific client, pick the first one available after System (0) */
@@ -327,7 +472,7 @@ NativeMidiSong *native_midi_loadsong_IO(SDL_IOStream *src, bool closeio)
     }
 
     /* Only allow echo events to be sent */
-    snd_seq_set_client_event_filter(song->seq, SND_SEQ_EVENT_ECHO);
+    ALSA_snd_seq_set_client_event_filter(song->seq, SND_SEQ_EVENT_ECHO);
 
     pick_seq_dest_addr(song);
 
@@ -367,9 +512,9 @@ static SDL_INLINE void enqueue_echo_event(const NativeMidiSong *song, const int
     snd_seq_ev_clear(&evt);
     evt.type = SND_SEQ_EVENT_ECHO;
     snd_seq_ev_set_source(&evt, song->srcport);
-    snd_seq_ev_set_dest(&evt, snd_seq_client_id(song->seq), song->srcport);
+    snd_seq_ev_set_dest(&evt, ALSA_snd_seq_client_id(song->seq), song->srcport);
     snd_seq_ev_schedule_tick(&evt, queue, 0, song->endtime + 1);
-    while (snd_seq_event_output(song->seq, &evt) == -EAGAIN)
+    while (ALSA_snd_seq_event_output(song->seq, &evt) == -EAGAIN)
         ;
 }
 
@@ -383,7 +528,7 @@ static SDL_INLINE void enqueue_queue_reset_event(const NativeMidiSong *song, con
     /* Schedule it to some point in the past, so that it is guaranteed */
     /* to run immediately and before the echo */
     snd_seq_ev_schedule_tick(&evt, queue, 0, 0);
-    while (snd_seq_event_output(song->seq, &evt) == -EAGAIN)
+    while (ALSA_snd_seq_event_output(song->seq, &evt) == -EAGAIN)
         ;
 }
 
@@ -398,7 +543,7 @@ static SDL_INLINE void send_volume_sysex(const NativeMidiSong *song, const unsig
     snd_seq_ev_set_dest(&evt, song->dstaddr.client, song->dstaddr.port);
     snd_seq_ev_set_direct(&evt);
     snd_seq_ev_set_sysex(&evt, sizeof(vol_sysex), vol_sysex);
-    snd_seq_event_output_direct(song->seq, &evt);
+    ALSA_snd_seq_event_output_direct(song->seq, &evt);
 }
 
 /* Sequencer queue control */
@@ -408,7 +553,7 @@ static SDL_INLINE void stop_queue(const NativeMidiSong *song, const int queue)
     snd_seq_ev_clear(&evt);
     snd_seq_ev_set_queue_control(&evt, SND_SEQ_EVENT_STOP, queue, 0);
     snd_seq_ev_set_direct(&evt);
-    snd_seq_event_output_direct(song->seq, &evt);
+    ALSA_snd_seq_event_output_direct(song->seq, &evt);
 }
 
 static SDL_INLINE void continue_queue(const NativeMidiSong *song, const int queue)
@@ -417,7 +562,7 @@ static SDL_INLINE void continue_queue(const NativeMidiSong *song, const int queu
     snd_seq_ev_clear(&evt);
     snd_seq_ev_set_queue_control(&evt, SND_SEQ_EVENT_CONTINUE, queue, 0);
     snd_seq_ev_set_direct(&evt);
-    snd_seq_event_output_direct(song->seq, &evt);
+    ALSA_snd_seq_event_output_direct(song->seq, &evt);
 }
 
 /* Playback thread */
@@ -428,7 +573,7 @@ static int native_midi_player_thread(void *d)
     NativeMidiSong *song = d;
     MIDIEvent *event = song->evtlist;
 
-    int queue = snd_seq_alloc_named_queue(song->seq, "SDL_Mixer Playback");
+    int queue = ALSA_snd_seq_alloc_named_queue(song->seq, "SDL_Mixer Playback");
     snd_seq_start_queue(song->seq, queue, NULL);
 
     /* Prepare main sequencer event */
@@ -442,15 +587,15 @@ static int native_midi_player_thread(void *d)
         .fd = song->threadsock,
         .events = POLLIN,
     } };
-    snd_seq_poll_descriptors(song->seq, pfds + 1, 1, POLLIN | POLLOUT);
-    snd_seq_nonblock(song->seq, 1);
+    ALSA_snd_seq_poll_descriptors(song->seq, pfds + 1, 1, POLLIN | POLLOUT);
+    ALSA_snd_seq_nonblock(song->seq, 1);
 
     /* Set initial queue tempo and ppqn */
     snd_seq_queue_tempo_t *tempo;
     snd_seq_queue_tempo_alloca(&tempo);
-    snd_seq_queue_tempo_set_tempo(tempo, 500000);
-    snd_seq_queue_tempo_set_ppq(tempo, song->ppqn);
-    snd_seq_set_queue_tempo(song->seq, queue, tempo);
+    ALSA_snd_seq_queue_tempo_set_tempo(tempo, 500000);
+    ALSA_snd_seq_queue_tempo_set_ppq(tempo, song->ppqn);
+    ALSA_snd_seq_set_queue_tempo(song->seq, queue, tempo);
 
     /* We use this to know when the track has finished playing */
     enqueue_echo_event(song, queue);
@@ -501,7 +646,7 @@ static int native_midi_player_thread(void *d)
         if (pfds[1].revents & POLLIN) {
             snd_seq_event_t *revt;
             /* Make sure we read an echo event, and that it came from us */
-            if (snd_seq_event_input(song->seq, &revt) >= 0 && revt->type == SND_SEQ_EVENT_ECHO && revt->source.client == snd_seq_client_id(song->seq) && revt->source.port == song->srcport)
+            if (ALSA_snd_seq_event_input(song->seq, &revt) >= 0 && revt->type == SND_SEQ_EVENT_ECHO && revt->source.client == ALSA_snd_seq_client_id(song->seq) && revt->source.port == song->srcport)
                 playback_finished = true;
         }
 
@@ -532,7 +677,7 @@ static int native_midi_player_thread(void *d)
                 /* If not, keep draining, otherwise we'll never reach the echo event */
                 /* When we finish though, prevent any "ready to write to alsa" polls */
                 MIDIDbgLog("Draining output!");
-                if (snd_seq_drain_output(song->seq) == 0)
+                if (ALSA_snd_seq_drain_output(song->seq) == 0)
                     pfds[1].events &= ~POLLOUT;
                 continue;
             }
@@ -593,8 +738,8 @@ static int native_midi_player_thread(void *d)
             unhandled = true;
         }
 
-        if (unhandled || snd_seq_event_output(song->seq, &evt) != -EAGAIN) {
-            MIDIDbgLog("%s %"SDL_PRIu32": %hhx %hhx %hhx (extraLen %"SDL_PRIu32")", (unhandled ? "Unhandled" : "Event"), event->time, event->status, event->data[0], event->data[1], event->extraLen);
+        if (unhandled || ALSA_snd_seq_event_output(song->seq, &evt) != -EAGAIN) {
+            MIDIDbgLog("%s %" SDL_PRIu32 ": %hhx %hhx %hhx (extraLen %" SDL_PRIu32 ")", (unhandled ? "Unhandled" : "Event"), event->time, event->status, event->data[0], event->data[1], event->extraLen);
             event = event->next;
         }
     }
@@ -602,24 +747,24 @@ static int native_midi_player_thread(void *d)
     SDL_SetAtomicInt(&song->playerstate, NATIVE_MIDI_STOPPED);
 
     /* Switch back to blocking mode and drop everything */
-    snd_seq_nonblock(song->seq, 0);
-    snd_seq_drop_output(song->seq);
+    ALSA_snd_seq_nonblock(song->seq, 0);
+    ALSA_snd_seq_drop_output(song->seq);
     snd_seq_stop_queue(song->seq, queue, NULL);
-    snd_seq_drain_output(song->seq);
-    snd_seq_free_queue(song->seq, queue);
+    ALSA_snd_seq_drain_output(song->seq);
+    ALSA_snd_seq_free_queue(song->seq, queue);
 
     /* Stop all audio */
     /* Some of these are bound to work */
     snd_seq_ev_set_direct(&evt);
     for (int i = 0; i < MIDI_CHANNELS; i++) {
         snd_seq_ev_set_controller(&evt, i, MIDI_CTL_SUSTAIN, 0);
-        snd_seq_event_output_direct(song->seq, &evt);
+        ALSA_snd_seq_event_output_direct(song->seq, &evt);
         snd_seq_ev_set_controller(&evt, i, MIDI_CTL_ALL_NOTES_OFF, 0);
-        snd_seq_event_output_direct(song->seq, &evt);
+        ALSA_snd_seq_event_output_direct(song->seq, &evt);
         snd_seq_ev_set_controller(&evt, i, MIDI_CTL_RESET_CONTROLLERS, 0);
-        snd_seq_event_output_direct(song->seq, &evt);
+        ALSA_snd_seq_event_output_direct(song->seq, &evt);
         snd_seq_ev_set_controller(&evt, i, MIDI_CTL_ALL_SOUNDS_OFF, 0);
-        snd_seq_event_output_direct(song->seq, &evt);
+        ALSA_snd_seq_event_output_direct(song->seq, &evt);
     }
 
     MIDIDbgLog("Playback thread returns");