SDL_mixer: native_midi_mint: add new backend

From eb3e457a8850d4c9c4f52e8818eaf32fee7ce9c1 Mon Sep 17 00:00:00 2001
From: Miro Kropacek <[EMAIL REDACTED]>
Date: Tue, 22 Sep 2026 11:12:57 +1000
Subject: [PATCH] native_midi_mint: add new backend

MIDI events are clocked out from the ACIA transmit interrupt for
accurate timing. Supports looping, several songs at once and volume
control.
---
 configure                           |  13 +
 configure.ac                        |  13 +
 native_midi/mint_asm_symbols.h      |  54 +++
 native_midi/native_midi_mint.c      | 517 ++++++++++++++++++++++++++++
 native_midi/native_midi_mint_xbra.S |  51 +++
 5 files changed, 648 insertions(+)
 create mode 100644 native_midi/mint_asm_symbols.h
 create mode 100644 native_midi/native_midi_mint.c
 create mode 100644 native_midi/native_midi_mint_xbra.S

diff --git a/configure b/configure
index 19d4d3c1e..ab0e4c6bc 100755
--- a/configure
+++ b/configure
@@ -15877,6 +15877,10 @@ fi
                 EXTRA_LDFLAGS="$EXTRA_LDFLAGS -lmidi"
                 PC_LIBS="$PC_LIBS -lmidi"
                 ;;
+            *-*-mint*)
+                use_music_native_midi=yes
+                SOURCES_ASM="$SOURCES_ASM $srcdir/native_midi/*.S"
+                ;;
         esac
         if test x$use_music_native_midi = xyes; then
             EXTRA_CFLAGS="$EXTRA_CFLAGS -DUSE_NATIVE_MIDI -I\$(srcdir)/native_midi"
@@ -17075,6 +17079,15 @@ $(objects)/\2.lo: \1/\2.cpp \$(objects)/.created\\
 	\$(LIBTOOL) --mode=compile \$(CXX) \$(CFLAGS) \$(EXTRA_CFLAGS) '"$DEPENDENCY_TRACKING_OPTIONS"' -c \$< -o \$@,g'`
 OBJECTS="$OBJECTS $OBJECTS_CXX"
 DEPENDS="$DEPENDS $DEPENDS_CXX"
+
+OBJECTS_ASM=`echo $SOURCES_ASM`
+DEPENDS_ASM=`echo $SOURCES_ASM`
+OBJECTS_ASM=`echo "$OBJECTS_ASM" | sed 's,[^ ]*/\([^ ]*\)\.S,$(objects)/\1.lo,g'`
+DEPENDS_ASM=`echo "$DEPENDS_ASM" | sed 's,\([^ ]*\)/\([^ ]*\)\.S,\\
+$(objects)/\2.lo: \1/\2.S \$(objects)/.created\\
+	\$(LIBTOOL) --mode=compile \$(CC) \$(CFLAGS) \$(EXTRA_CFLAGS) -c \$< -o \$@,g'`
+OBJECTS="$OBJECTS $OBJECTS_ASM"
+DEPENDS="$DEPENDS $DEPENDS_ASM"
 DEPENDS=`echo "$DEPENDS" | sed 's,\\$,\\\\$,g'`
 
 VERSION_OBJECTS=`echo $VERSION_SOURCES`
diff --git a/configure.ac b/configure.ac
index a2128e49e..8c9619a1e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -400,6 +400,10 @@ if test x$enable_music_midi = xyes; then
                 EXTRA_LDFLAGS="$EXTRA_LDFLAGS -lmidi" 
                 PC_LIBS="$PC_LIBS -lmidi"
                 ;;
+            *-*-mint*)
+                use_music_native_midi=yes
+                SOURCES_ASM="$SOURCES_ASM $srcdir/native_midi/*.S"
+                ;;
         esac
         if test x$use_music_native_midi = xyes; then
             EXTRA_CFLAGS="$EXTRA_CFLAGS -DUSE_NATIVE_MIDI -I\$(srcdir)/native_midi"
@@ -724,6 +728,15 @@ $(objects)/\2.lo: \1/\2.cpp \$(objects)/.created\\
 	\$(LIBTOOL) --mode=compile \$(CXX) \$(CFLAGS) \$(EXTRA_CFLAGS) '"$DEPENDENCY_TRACKING_OPTIONS"' -c \$< -o \$@,g'`
 OBJECTS="$OBJECTS $OBJECTS_CXX"
 DEPENDS="$DEPENDS $DEPENDS_CXX"
+
+OBJECTS_ASM=`echo $SOURCES_ASM`
+DEPENDS_ASM=`echo $SOURCES_ASM`
+OBJECTS_ASM=`echo "$OBJECTS_ASM" | sed 's,[[^ ]]*/\([[^ ]]*\)\.S,$(objects)/\1.lo,g'`
+DEPENDS_ASM=`echo "$DEPENDS_ASM" | sed 's,\([[^ ]]*\)/\([[^ ]]*\)\.S,\\
+$(objects)/\2.lo: \1/\2.S \$(objects)/.created\\
+	\$(LIBTOOL) --mode=compile \$(CC) \$(CFLAGS) \$(EXTRA_CFLAGS) -c \$< -o \$@,g'`
+OBJECTS="$OBJECTS $OBJECTS_ASM"
+DEPENDS="$DEPENDS $DEPENDS_ASM"
 DEPENDS=`echo "$DEPENDS" | sed 's,\\$,\\\\$,g'`
 
 VERSION_OBJECTS=`echo $VERSION_SOURCES`
diff --git a/native_midi/mint_asm_symbols.h b/native_midi/mint_asm_symbols.h
new file mode 100644
index 000000000..e407e1471
--- /dev/null
+++ b/native_midi/mint_asm_symbols.h
@@ -0,0 +1,54 @@
+#ifndef __USER_LABEL_PREFIX__
+#define __USER_LABEL_PREFIX__ _
+#endif
+
+#ifndef __REGISTER_PREFIX__
+#define __REGISTER_PREFIX__
+#endif
+
+#ifndef __IMMEDIATE_PREFIX__
+#define __IMMEDIATE_PREFIX__ #
+#endif
+
+#define CONCAT1(a, b) CONCAT2(a, b)
+#define CONCAT2(a, b) a ## b
+
+/* Use the right prefix for global labels.  */
+
+#define SYM(x) CONCAT1 (__USER_LABEL_PREFIX__, x)
+
+#ifdef __ELF__
+#define FUNC(x) .type SYM(x),function
+#else
+/* The .proc pseudo-op is accepted, but ignored, by GAS.  We could just	
+   define this to the empty string for non-ELF systems, but defining it
+   to .proc means that the information is available to the assembler if
+   the need arises.  */
+#define FUNC(x) .proc
+#endif
+		
+#define REG(x) CONCAT1 (__REGISTER_PREFIX__, x)
+
+#define IMM(x) CONCAT1 (__IMMEDIATE_PREFIX__, x)
+
+#define d0 REG(d0)
+#define d1 REG(d1)
+#define d2 REG(d2)
+#define d3 REG(d3)
+#define d4 REG(d4)
+#define d5 REG(d5)
+#define d6 REG(d6)
+#define d7 REG(d7)
+#define a0 REG(a0)
+#define a1 REG(a1)
+#define a2 REG(a2)
+#define a3 REG(a3)
+#define a4 REG(a4)
+#define a5 REG(a5)
+#define a6 REG(a6)
+#define a7 REG(a7)
+#define fp REG(fp)
+#define sp REG(sp)
+#define pc REG(pc)
+
+#define sr REG(sr)
diff --git a/native_midi/native_midi_mint.c b/native_midi/native_midi_mint.c
new file mode 100644
index 000000000..307599263
--- /dev/null
+++ b/native_midi/native_midi_mint.c
@@ -0,0 +1,517 @@
+/*
+  native_midi:  Hardware Midi support on Atari for the SDL_mixer library
+  Copyright (C) 2026  Miro Kropacek <miro.kropacek@gmail.com>
+
+  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 __MINT__
+
+#include <stdlib.h>
+
+#include <mint/osbind.h>
+#include <mint/ostruct.h>
+
+#include "../SDL_mixer.h"
+
+#include "native_midi.h"
+#include "native_midi_common.h"
+
+/*
+ * Two interrupt sources, one FIFO between them:
+ *
+ * 1. Timer B is the PRODUCER, running at a constant 960 Hz (2457600 /
+ *    16 / 160), i.e. ~1 ms event timing resolution. Each interrupt adds
+ *    a 16.16 fixed-point increment (MIDI ticks per timer period, derived
+ *    from ticksPerQuarterNote and the current tempo) to the song
+ *    position and pushes the bytes of events that became due into the
+ *    FIFO. A tempo change merely recomputes the increment, so any tempo
+ *    and any tick rate are representable exactly. A full FIFO pauses
+ *    event processing until space frees up; bytes are never dropped.
+ *
+ * 2. The MIDI ACIA transmit interrupt is the CONSUMER, draining the
+ *    FIFO at wire speed (31250 baud / 10 bits = 3125 bytes/s), one
+ *    interrupt per byte and none when idle. Both ACIAs share MFP GPIP
+ *    I4 (channel 6, vector $118); the TOS handler ("midikey") loops
+ *    calling kb_midisys and kb_ikbdsys until the interrupt line is
+ *    released, so we hook kb_midisys via Kbdvbase() and chain the
+ *    original for the receive side. The transmit interrupt (TIE) is
+ *    enabled ONLY while the FIFO is non-empty: an empty transmit
+ *    register asserts IRQ permanently, and since nothing would clear
+ *    it, the midikey loop would hang the machine. The hook therefore
+ *    always either sends a byte or turns TIE off before returning.
+ *
+ * Volume: a MIDI song controls 16 channel volumes (controller 7) but
+ * the wire has no master volume, so native_midi_setvolume() (called
+ * from music.c for Mix_VolumeMusic() and for fades) implements one by
+ * scaling. s_lastCC7[] holds each channel volume as the song last set
+ * it: the producer updates it from controller 7 events and transmits
+ * s_lastCC7[channel] * s_volume / MIX_MAX_VOLUME. native_midi_setvolume()
+ * stores s_volume, sets s_volDirty and returns; the producer then queues
+ * controller 7 messages with the recomputed values for all 16 channels.
+ */
+
+#define MIDI_ACIA_CTRL  (*(volatile Uint8 *)0xFFFFFC04L)
+#define MIDI_ACIA_DATA  (*(volatile Uint8 *)0xFFFFFC06L)
+#define ACIA_TDRE       (1 << 1)
+
+/* /16 clock, 8N1, RTS low, TX interrupt off, RX interrupt on.*/
+#define ACIA_CTRL_TIE_OFF   0x95
+/* /16 clock, 8N1, RTS low, TX interrupt on, RX interrupt on.*/
+#define ACIA_CTRL_TIE_ON    0xB5
+
+#define MFP_IERA        (*(volatile Uint8 *)0xFFFFFA07L)
+#define MFP_IPRA        (*(volatile Uint8 *)0xFFFFFA0BL)
+#define MFP_ISRA        (*(volatile Uint8 *)0xFFFFFA0FL)
+#define MFP_IMRA        (*(volatile Uint8 *)0xFFFFFA13L)
+#define MFP_TBCR        (*(volatile Uint8 *)0xFFFFFA1BL)
+
+#define MFP_CLOCK       2457600UL
+#define TIMER_B_CTRL    3       /* prescaler /16 */
+#define TIMER_B_PRESCALER 16
+#define TIMER_B_DATA    160     /* 2457600/16/160 = 960 Hz */
+
+#define TICK_NUMER      ((Uint32)((65536ULL \
+                                       * TIMER_B_PRESCALER \
+                                       * TIMER_B_DATA \
+                                       * 1000000 \
+                                       + MFP_CLOCK/2) / MFP_CLOCK))
+
+#define DEFAULT_TEMPO   500000UL    /* us per quarter note (120 bpm) */
+#define DEFAULT_CC7     100         /* GM default channel volume */
+
+#define FIFO_SIZE       2048        /* power of two */
+#define FIFO_MASK       (FIFO_SIZE - 1)
+
+struct _NativeMidiSong
+{
+    MIDIEvent *firstEvent;
+    Uint16 ticksPerQuarterNote; /* 48 by default (independent of the tempo) */
+};
+
+/* Only one song plays at a time; the playback state lives here and not
+ * in NativeMidiSong. */
+static NativeMidiSong * volatile s_currentSong; /* NULL when stopped */
+static MIDIEvent *s_events;                     /* next event to schedule */
+static Uint32 s_ticksPerQuarterNote;
+static int s_loops;
+
+/* 16.16 fixed point: upper 16 bits integer part, lower 16 bits fraction.
+ * Every timer interrupt executes s_tickFrac += s_tickAdd; whenever
+ * s_tickFrac crosses 1.0, the whole part moves into s_tickInt and only
+ * the fraction stays. */
+static Uint32 s_tickAdd;
+static Uint32 s_tickFrac;
+static Uint32 s_tickInt;
+
+static Uint8 s_fifo[FIFO_SIZE];
+static volatile Uint16 s_fifoHead, s_fifoTail;
+
+static Uint8 s_lastCC7[16];         /* channel volume as sent by the song */
+static volatile Uint8 s_volume = MIX_MAX_VOLUME;    /* master volume */
+static volatile SDL_bool s_volDirty;        /* set by native_midi_setvolume(),
+                                               consumed by the producer */
+
+static volatile SDL_bool s_tie;     /* shadow: is the TX interrupt enabled? */
+static _KBDVECS *s_kbdvecs;
+
+/* private SDL API (src/video/ataricommon/SDL_atarixbra.c) */
+typedef void (*XbraHandler)(void);
+extern XbraHandler Atari_UnhookXbra(Uint32 vecnum, Uint32 app_id, XbraHandler handler);
+
+#define XBRA_APP_ID     0x4C53444DUL    /* 'LSDM' */
+
+/* native_midi_mint_xbra.S */
+extern void midisys_handler(void);
+extern XbraHandler midisys_oldvec;
+
+static Uint32 s_oldTimerbVec;
+static Uint8 s_oldTbcr;
+static SDL_bool s_oldTimerbEnabled, s_oldTimerbMasked;
+
+static __inline__ Uint16 set_ipl7(void)
+{
+    Uint16 sr;
+#ifdef __mcoldfire__
+    /* ColdFire has no ori to SR; this runs in supervisor mode, so
+     * setting the S bit along with the mask is safe */
+    __asm__ volatile ("move.w %%sr,%0\n\tmove.w #0x2700,%%sr" : "=d"(sr) : : "memory");
+#else
+    __asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d"(sr) : : "memory");
+#endif
+    return sr;
+}
+
+static __inline__ void restore_ipl(Uint16 sr)
+{
+    __asm__ volatile ("move.w %0,%%sr" : : "d"(sr) : "memory");
+}
+
+static __inline__ Uint8 scaled_cc7(Uint8 chan)
+{
+    return (Uint16)(s_lastCC7[chan] * s_volume) / MIX_MAX_VOLUME;
+}
+
+static __inline__ Uint16 fifo_space(void)
+{
+    return FIFO_SIZE - 1 - ((s_fifoHead - s_fifoTail) & FIFO_MASK);
+}
+
+static __inline__ void fifo_push(Uint8 b)
+{
+    s_fifo[s_fifoHead] = b;
+    s_fifoHead = (s_fifoHead + 1) & FIFO_MASK;
+}
+
+/* Returns how many MIDI ticks elapse during one timer period, as 16.16
+ * fixed point: 65536 * ticksPerQuarterNote * period_us / tempo_us. */
+static __inline__ Uint32 midi_ticks_per_period(Uint32 tempo)
+{
+    Uint32 q, r;
+
+    q = TICK_NUMER / tempo;
+    r = TICK_NUMER % tempo;
+
+    return s_ticksPerQuarterNote * q
+         + (s_ticksPerQuarterNote * r + tempo / 2) / tempo;
+}
+
+/* Producer: schedule due events into the FIFO, arm the ACIA TX interrupt */
+static void __attribute__((interrupt)) timer_b(void)
+{
+    MIDIEvent *ev;
+
+    /* advance song position */
+    s_tickFrac += s_tickAdd;
+    s_tickInt += s_tickFrac >> 16;
+    s_tickFrac &= 0xffff;
+
+    /* schedule due events */
+    ev = s_events;
+    while (ev && ev->time <= s_tickInt)
+    {
+        const Uint8 status = ev->status;
+
+        if (status == 0xff)
+        {
+            /* Meta event; never sent over the wire */
+            if (ev->data[0] == 0x51 && ev->extraLen == 3)
+            {
+                /* Tempo change */
+                Uint32 tempo = ((Uint32)ev->extraData[0] << 16)
+                             | ((Uint32)ev->extraData[1] << 8)
+                             |  (Uint32)ev->extraData[2];
+                if (tempo)
+                    s_tickAdd = midi_ticks_per_period(tempo);
+            }
+            /* 0x2f (end of track) and the rest are ignored;
+             * end of song == end of the event list */
+        }
+        else
+        {
+            switch (status >> 4)
+            {
+            case MIDI_STATUS_CONTROLLER:
+                if (fifo_space() < 3)
+                    goto fifo_full;
+                if (ev->data[0] == 0x07)
+                {
+                    /* Channel volume: remember it and send it scaled
+                     * by the master volume */
+                    s_lastCC7[status & 0x0f] = ev->data[1];
+                    fifo_push(status);
+                    fifo_push(0x07);
+                    fifo_push(scaled_cc7(status & 0x0f));
+                    break;
+                }
+                /* fall through */
+            case MIDI_STATUS_NOTE_OFF:
+            case MIDI_STATUS_NOTE_ON:
+            case MIDI_STATUS_AFTERTOUCH:
+            case MIDI_STATUS_PITCH_WHEEL:
+                if (fifo_space() < 3)
+                    goto fifo_full;
+                fifo_push(status);
+                fifo_push(ev->data[0]);
+                fifo_push(ev->data[1]);
+                break;
+
+            case MIDI_STATUS_PROG_CHANGE:
+            case MIDI_STATUS_PRESSURE:
+                if (fifo_space() < 2)
+                    goto fifo_full;
+                fifo_push(status);
+                fifo_push(ev->data[0]);
+                break;
+
+            default:
+                /* Sysex (0xf0/0xf7): skipped */
+                break;
+            }
+        }
+
+        ev = ev->next;
+    }
+fifo_full:
+    s_events = ev;
+
+    if (!ev && s_loops)
+    {
+        if (s_loops > 0)
+            s_loops--;
+
+        s_events    = s_currentSong->firstEvent;
+        s_tickAdd   = midi_ticks_per_period(DEFAULT_TEMPO);
+        s_tickFrac  = 0;
+        s_tickInt   = 0;
+    }
+
+    /* Master volume change: resend the scaled channel volumes */
+    if (s_volDirty && fifo_space() >= 16 * 3)
+    {
+        int i;
+
+        s_volDirty = SDL_FALSE;
+        for (i = 0; i < 16; ++i)
+        {
+            fifo_push(0xb0 | i);
+            fifo_push(0x07);
+            fifo_push(scaled_cc7(i));
+        }
+    }
+
+    /* bytes queued and TX interrupt not armed: arm it. If TDRE is
+     * already set this asserts the ACIA IRQ at once (falling edge on
+     * GPIP I4), so transmission starts right after we return. */
+    if (s_fifoHead != s_fifoTail && !s_tie)
+    {
+        s_tie = SDL_TRUE;
+        MIDI_ACIA_CTRL = ACIA_CTRL_TIE_ON;
+    }
+
+    MFP_ISRA = ~(1 << 0);   /* clear in service bit */
+}
+
+/* kb_midisys hook: called (jsr, supervisor) by the TOS $118 handler on
+ * every ACIA interrupt, in a loop until the interrupt line is released.
+ * Send one byte per TDRE, disarm TIE the moment the FIFO runs dry --
+ * leaving TIE armed with nothing to send would hang the midikey loop. */
+void midi_acia_hook(void)
+{
+    if (s_tie && (MIDI_ACIA_CTRL & ACIA_TDRE))
+    {
+        if (s_fifoHead != s_fifoTail)
+        {
+            MIDI_ACIA_DATA = s_fifo[s_fifoTail];
+            s_fifoTail = (s_fifoTail + 1) & FIFO_MASK;
+        }
+        else
+        {
+            MIDI_ACIA_CTRL = ACIA_CTRL_TIE_OFF;
+            s_tie = SDL_FALSE;
+        }
+    }
+}
+
+static long hook_install(void)
+{
+    Uint16 sr = set_ipl7();
+
+    s_oldTbcr = MFP_TBCR;
+    s_oldTimerbEnabled = (MFP_IERA & (1 << 0)) != 0;
+    s_oldTimerbMasked  = (MFP_IMRA & (1 << 0)) != 0;
+
+    s_tie = SDL_FALSE;
+    MIDI_ACIA_CTRL = ACIA_CTRL_TIE_OFF;
+
+    if ((XbraHandler)s_kbdvecs->midisys != midisys_handler)
+    {
+        midisys_oldvec = (XbraHandler)s_kbdvecs->midisys;
+        s_kbdvecs->midisys = (long (*)(void))midisys_handler;
+    }
+
+    restore_ipl(sr);
+
+    return 0;
+}
+
+static void acia_send(Uint8 b)
+{
+    while ((MIDI_ACIA_CTRL & ACIA_TDRE) == 0)
+        ;
+    MIDI_ACIA_DATA = b;
+}
+
+static long hook_remove(void)
+{
+    Uint16 sr;
+    int i;
+
+    /* order matters: hardware TIE off first, only then the shadow */
+    sr = set_ipl7();
+    MIDI_ACIA_CTRL = ACIA_CTRL_TIE_OFF;
+    s_tie = SDL_FALSE;
+    restore_ipl(sr);
+
+    /* Silence every channel (TIE is off, so polled sending cannot race
+     * the consumer; interrupts stay enabled, this takes ~30 ms).
+     * A status byte aborts a possibly
+     * half-transmitted message, so the stream stays parseable. */
+    for (i = 0; i < 16; ++i)
+    {
+        acia_send(0xb0 | i);
+        acia_send(0x78);    /* All Sound Off */
+        acia_send(0x00);
+        acia_send(0xb0 | i);
+        acia_send(0x7b);    /* All Notes Off */
+        acia_send(0x00);
+    }
+
+    sr = set_ipl7();
+
+    Atari_UnhookXbra((Uint32)&s_kbdvecs->midisys, XBRA_APP_ID, midisys_handler);
+
+    MFP_TBCR = s_oldTbcr;
+    MFP_IPRA = (Uint8)~(1 << 0);    /* discard a pending tick of ours */
+    if (s_oldTimerbEnabled)
+        MFP_IERA |= 1 << 0;
+    if (s_oldTimerbMasked)
+        MFP_IMRA |= 1 << 0;
+
+    restore_ipl(sr);
+
+    return 0;
+}
+
+int native_midi_detect()
+{
+    return 1;  /* always available */
+}
+
+NativeMidiSong *native_midi_loadsong_RW(SDL_RWops *rw, int freerw)
+{
+    NativeMidiSong *song;
+
+    printf("%s\n", __FUNCTION__);
+
+    song = (NativeMidiSong *)malloc(sizeof(*song));
+    if (song)
+    {
+        song->firstEvent = CreateMIDIEventList(rw, &song->ticksPerQuarterNote);
+        if (!song->firstEvent)
+        {
+            free(song);
+            song = NULL;
+        }
+    }
+
+    if (freerw)
+        SDL_RWclose(rw);
+
+    return song;
+}
+
+void native_midi_freesong(NativeMidiSong *song)
+{
+    printf("%s\n", __FUNCTION__);
+
+    if (!song)
+        return;
+
+    if (song == s_currentSong)
+        native_midi_stop();
+
+    FreeMIDIEventList(song->firstEvent);
+    free(song);
+}
+
+void native_midi_start(NativeMidiSong *song, int loops)
+{
+    int i;
+
+    printf("%s: %d\n", __FUNCTION__, loops);
+
+    if (s_currentSong)
+        native_midi_stop();
+
+    s_events = song->firstEvent;
+    s_ticksPerQuarterNote = song->ticksPerQuarterNote;
+
+    s_tickAdd    = midi_ticks_per_period(DEFAULT_TEMPO);
+    s_tickFrac   = 0;
+    s_tickInt    = 0;
+    s_fifoHead   = s_fifoTail = 0;
+
+    for (i = 0; i < 16; ++i)
+        s_lastCC7[i] = DEFAULT_CC7;
+    s_volDirty = SDL_TRUE;      /* apply the master volume from the start */
+
+    s_kbdvecs = Kbdvbase();
+    Supexec(hook_install);
+
+    s_loops = loops;    /* number of repeats after the first pass; -1 = forever */
+
+    s_currentSong = song;
+
+    Jdisint(MFP_TIMERB);
+    s_oldTimerbVec = (Uint32)Setexc(0x120>>2, -1);
+    Xbtimer(XB_TIMERB, TIMER_B_CTRL, TIMER_B_DATA, timer_b);
+}
+
+void native_midi_stop()
+{
+    printf("%s\n", __FUNCTION__);
+
+    Jdisint(MFP_TIMERB);
+    if (s_oldTimerbVec)
+    {
+        (void)Setexc(0x120>>2, s_oldTimerbVec);
+        s_oldTimerbVec = 0;
+    }
+
+    if (s_kbdvecs)
+        Supexec(hook_remove);
+
+    s_currentSong = NULL;
+}
+
+int native_midi_active()
+{
+    return s_currentSong != NULL
+        && (s_events || s_fifoHead != s_fifoTail);
+}
+
+void native_midi_setvolume(int volume)
+{
+    printf("%s: %d\n", __FUNCTION__, volume);
+
+    if (volume < 0)
+        volume = 0;
+    else if (volume > MIX_MAX_VOLUME)
+        volume = MIX_MAX_VOLUME;
+
+    s_volume = volume;
+    s_volDirty = SDL_TRUE;
+}
+
+const char *native_midi_error(void)
+{
+    return "";
+}
+
+#endif  /* __MINT__ */
diff --git a/native_midi/native_midi_mint_xbra.S b/native_midi/native_midi_mint_xbra.S
new file mode 100644
index 000000000..50fd218c0
--- /dev/null
+++ b/native_midi/native_midi_mint_xbra.S
@@ -0,0 +1,51 @@
+/*
+  native_midi:  Hardware Midi support on Atari for the SDL_mixer library
+  Copyright (C) 2026  Miro Kropacek <miro.kropacek@gmail.com>
+
+  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.
+*/
+
+#ifdef __MINT__
+
+#include "mint_asm_symbols.h"
+
+/*
+ * kb_midisys entry with an XBRA header: calls the C handler for the
+ * transmit side, then jumps to the previous vector for the receive
+ * side.
+ *
+ * No specification defines which registers a kbdvecs routine may
+ * clobber, but both Atari TOS and EmuTOS save d0-d3/a0-a3 around these
+ * calls. This handler clobbers only d0-d1/a0-a1 (C call) plus a0
+ * (chain jump), so nothing has to be saved here under either reading.
+ */
+
+	.globl	SYM(midisys_oldvec)
+	.globl	SYM(midisys_handler)
+
+	.text
+
+	.ascii	"XBRA"
+	.ascii	"LSDM"
+SYM(midisys_oldvec):
+	.long	0
+SYM(midisys_handler):
+	jsr		SYM(midi_acia_hook)
+	move.l	SYM(midisys_oldvec),a0
+	jmp		(a0)
+
+#endif