std1.0.0-beta-7 SafeC docs

Source audio/midi.sc

View in the repository

// SafeC Standard Library — real-time MIDI I/O: the API-independent core
// (see midi.h): the API registry, message helpers, the parser/encoder,
// the lock-free input queue and the MidiIn/MidiOut front ends.
// safeguard: implements <std/audio/midi_backend.h>
#pragma once
#include <std/audio/midi.h>
#include <std/audio/midi_backend.h>
#include <std/audio/audio_sys.h>
#include <std/mem.h>

namespace std {

// ── API names & registry ─────────────────────────────────────────────────

const &char midi_api_name(int api) {
    if (api == MIDI_API_ALSA) return "alsa";
    if (api == MIDI_API_JACK) return "jack";
    if (api == MIDI_API_COREMIDI) return "coremidi";
    if (api == MIDI_API_WINMM) return "winmm";
    if (api == MIDI_API_WEBMIDI) return "webmidi";
    if (api == MIDI_API_DUMMY) return "dummy";
    return "";
}

const &char midi_api_display_name(int api) {
    if (api == MIDI_API_ALSA) return "ALSA";
    if (api == MIDI_API_JACK) return "JACK";
    if (api == MIDI_API_COREMIDI) return "Core MIDI";
    if (api == MIDI_API_WINMM) return "Windows MultiMedia";
    if (api == MIDI_API_WEBMIDI) return "Web MIDI";
    if (api == MIDI_API_DUMMY) return "Dummy";
    return "";
}

int midi_api_by_name(const &char name) {
    for (int a = 1; a < MIDI_API_COUNT; a = a + 1)
        if (audio_streq_(name, midi_api_name(a))) return a;
    return MIDI_API_UNSPECIFIED;
}

static int midi_backends_(&?&static MidiBackendVt_ out, int cap) {
    int n = 0;
#if defined(__APPLE__)
    if (n < cap) { unsafe { out[n] = (?&static MidiBackendVt_)midi_coremidi_backend_(); } n = n + 1; }
    if (n < cap) { unsafe { out[n] = (?&static MidiBackendVt_)midi_jack_backend_(); } n = n + 1; }
#endif
#if defined(__linux__)
    if (n < cap) { unsafe { out[n] = (?&static MidiBackendVt_)midi_alsa_backend_(); } n = n + 1; }
    if (n < cap) { unsafe { out[n] = (?&static MidiBackendVt_)midi_jack_backend_(); } n = n + 1; }
#endif
#if defined(_WIN32)
    if (n < cap) { unsafe { out[n] = (?&static MidiBackendVt_)midi_winmm_backend_(); } n = n + 1; }
#endif
#if defined(__wasm__)
    if (n < cap) { unsafe { out[n] = (?&static MidiBackendVt_)midi_webmidi_backend_(); } n = n + 1; }
#endif
    if (n < cap) { unsafe { out[n] = (?&static MidiBackendVt_)midi_dummy_backend_(); } n = n + 1; }
    return n;
}

static ?&static MidiBackendVt_ midi_backend_for_(int api) {
    ?&static MidiBackendVt_ list[MIDI_API_COUNT];
    int n = midi_backends_(&list[0], MIDI_API_COUNT);
    for (int i = 0; i < n; i = i + 1) {
        match (list[i]) {
            case some(vt): { if (vt.api == api) return (?&static MidiBackendVt_)vt; }
            case null: {}
        }
    }
    return null;
}

int midi_compiled_apis([]int out) {
    ?&static MidiBackendVt_ list[MIDI_API_COUNT];
    int n = midi_backends_(&list[0], MIDI_API_COUNT);
    int k = 0;
    for (int i = 0; i < n && (unsigned long)k < out.len; i = i + 1) {
        match (list[i]) {
            case some(vt): { out[k] = vt.api; k = k + 1; }
            case null: {}
        }
    }
    return k;
}

int midi_api_compiled(int api) { return !midi_backend_for_(api).is_null(); }

static int midi_available_(&MidiBackendVt_ vt) {
    int r = 0;
    unsafe { r = vt.available(); }
    return r;
}

int midi_default_api() {
    ?&char env = audio_getenv_("SAFEC_MIDI_API");
    match (env) {
        case some(e): {
            int a = midi_api_by_name(e);
            if (a != MIDI_API_UNSPECIFIED && midi_api_compiled(a)) return a;
        }
        case null: {}
    }
    ?&static MidiBackendVt_ list[MIDI_API_COUNT];
    int n = midi_backends_(&list[0], MIDI_API_COUNT);
    for (int i = 0; i < n; i = i + 1) {
        match (list[i]) {
            case some(vt): { if (vt.api == MIDI_API_JACK && midi_available_(vt)) return MIDI_API_JACK; }
            case null: {}
        }
    }
    for (int i = 0; i < n; i = i + 1) {
        match (list[i]) {
            case some(vt): { if (vt.api != MIDI_API_JACK && vt.api != MIDI_API_DUMMY) return vt.api; }
            case null: {}
        }
    }
    return MIDI_API_DUMMY;
}

// ── message helpers ──────────────────────────────────────────────────────

static int midi_clamp7_(int v) {
    if (v < 0) return 0;
    if (v > 127) return 127;
    return v;
}

static struct MidiMsg midi_msg3_(int status, int a, int b) {
    struct MidiMsg m;
    m.data[0] = (unsigned char)(status & 0xFF);
    m.data[1] = (unsigned char)midi_clamp7_(a);
    m.data[2] = (unsigned char)midi_clamp7_(b);
    m.len = 3;
    return m;
}

static struct MidiMsg midi_msg2_(int status, int a) {
    struct MidiMsg m;
    m.data[0] = (unsigned char)(status & 0xFF);
    m.data[1] = (unsigned char)midi_clamp7_(a);
    m.data[2] = (unsigned char)0;
    m.len = 2;
    return m;
}

static int midi_ch_(int channel) { return channel & 0x0F; }

struct MidiMsg MidiMsg::note_on(int channel, int note, int velocity) {
    return midi_msg3_(MIDI_NOTE_ON | midi_ch_(channel), note, velocity);
}
struct MidiMsg MidiMsg::note_off(int channel, int note, int velocity) {
    return midi_msg3_(MIDI_NOTE_OFF | midi_ch_(channel), note, velocity);
}
struct MidiMsg MidiMsg::poly_pressure(int channel, int note, int pressure) {
    return midi_msg3_(MIDI_POLY_PRESSURE | midi_ch_(channel), note, pressure);
}
struct MidiMsg MidiMsg::control_change(int channel, int controller, int value) {
    return midi_msg3_(MIDI_CONTROL_CHANGE | midi_ch_(channel), controller, value);
}
struct MidiMsg MidiMsg::program_change(int channel, int program) {
    return midi_msg2_(MIDI_PROGRAM_CHANGE | midi_ch_(channel), program);
}
struct MidiMsg MidiMsg::channel_pressure(int channel, int pressure) {
    return midi_msg2_(MIDI_CHANNEL_PRESSURE | midi_ch_(channel), pressure);
}
struct MidiMsg MidiMsg::pitch_bend(int channel, int value) {
    int v = value + 8192;
    if (v < 0) v = 0;
    if (v > 16383) v = 16383;
    return midi_msg3_(MIDI_PITCH_BEND | midi_ch_(channel), v & 0x7F, (v >> 7) & 0x7F);
}
struct MidiMsg MidiMsg::song_position(int beats) {
    int v = beats;
    if (v < 0) v = 0;
    if (v > 16383) v = 16383;
    return midi_msg3_(MIDI_SONG_POSITION, v & 0x7F, (v >> 7) & 0x7F);
}
struct MidiMsg MidiMsg::song_select(int song) { return midi_msg2_(MIDI_SONG_SELECT, song); }
struct MidiMsg MidiMsg::system(int status) {
    struct MidiMsg m;
    m.data[0] = (unsigned char)(status & 0xFF);
    m.data[1] = (unsigned char)0;
    m.data[2] = (unsigned char)0;
    m.len = 1;
    return m;
}
struct MidiMsg MidiMsg::of([]const unsigned char bytes) {
    struct MidiMsg m;
    m.data[0] = (unsigned char)0; m.data[1] = (unsigned char)0; m.data[2] = (unsigned char)0;
    m.len = 0;
    unsigned long n = bytes.len;
    if (n == 0UL || n > 3UL) return m;
    for (unsigned long i = 0UL; i < n; i = i + 1UL) m.data[i] = bytes[i];
    m.len = (int)n;
    return m;
}

[]const unsigned char MidiMsg::bytes() const {
    unsigned long n = self.len < 0 ? 0UL : (unsigned long)self.len;
    if (n > 3UL) n = 3UL;
    return self.data[0..n];
}

int MidiMsg::status() const { return self.len > 0 ? (int)self.data[0] : 0; }
int MidiMsg::type() const {
    int s = self.status();
    if (s >= 0x80 && s < 0xF0) return s & 0xF0;
    return s;
}
int MidiMsg::channel() const {
    int s = self.status();
    if (s >= 0x80 && s < 0xF0) return s & 0x0F;
    return -1;
}
int MidiMsg::data1() const { return self.len > 1 ? (int)self.data[1] : -1; }
int MidiMsg::data2() const { return self.len > 2 ? (int)self.data[2] : -1; }
int MidiMsg::is_note_on() const { return self.type() == MIDI_NOTE_ON && self.len == 3 && self.data[2] > (unsigned char)0; }
int MidiMsg::is_note_off() const {
    int t = self.type();
    return self.len == 3 && (t == MIDI_NOTE_OFF || (t == MIDI_NOTE_ON && self.data[2] == (unsigned char)0));
}
int MidiMsg::pitch_bend_value() const {
    if (self.type() != MIDI_PITCH_BEND || self.len != 3) return 0;
    return midi_14bit((int)self.data[1], (int)self.data[2]) - 8192;
}

int midi_message_length(int status) {
    if (status < 0x80 || status > 0xFF) return -1;
    if (status < 0xF0) {
        int t = status & 0xF0;
        if (t == MIDI_PROGRAM_CHANGE || t == MIDI_CHANNEL_PRESSURE) return 2;
        return 3;
    }
    if (status == MIDI_SYSEX) return 0;
    if (status == MIDI_TIME_CODE || status == MIDI_SONG_SELECT) return 2;
    if (status == MIDI_SONG_POSITION) return 3;
    if (status == 0xF4 || status == 0xF5 || status == 0xF9 || status == 0xFD) return -1;  // undefined
    return 1;   // F6, F7, real-time
}

int midi_is_realtime(int status) { return status >= 0xF8 && status <= 0xFF; }

long midi_build_sysex([]unsigned char out, []const unsigned char payload) {
    unsigned long n = payload.len;
    if (out.len < n + 2UL) return -1L;
    for (unsigned long i = 0UL; i < n; i = i + 1UL) if (payload[i] >= (unsigned char)0x80) return -1L;
    out[0] = (unsigned char)MIDI_SYSEX;
    for (unsigned long i = 0UL; i < n; i = i + 1UL) out[i + 1UL] = payload[i];
    out[n + 1UL] = (unsigned char)MIDI_SYSEX_END;
    return (long)(n + 2UL);
}

int midi_14bit(int lsb, int msb) { return (lsb & 0x7F) | ((msb & 0x7F) << 7); }

// ── parser ───────────────────────────────────────────────────────────────

struct MidiParser MidiParser::new([]unsigned char sysex_buf) {
    struct MidiParser p;
    p.running = (unsigned char)0;
    p.msg[0] = (unsigned char)0; p.msg[1] = (unsigned char)0; p.msg[2] = (unsigned char)0;
    p.have = 0;
    p.need = 0;
    p.last = MidiMsg::system(0);
    p.last.len = 0;
    p.sysex = sysex_buf;
    p.sysex_len = 0UL;
    p.in_sysex = 0;
    p.sysex_overflow = 0;
    return p;
}

void MidiParser::reset() {
    self.running = (unsigned char)0;
    self.have = 0;
    self.need = 0;
    self.sysex_len = 0UL;
    self.in_sysex = 0;
    self.sysex_overflow = 0;
}

static void midi_parser_sysex_put_(&MidiParser p, int byte) {
    if (p.sysex_len < p.sysex.len) {
        p.sysex[p.sysex_len] = (unsigned char)byte;
        p.sysex_len = p.sysex_len + 1UL;
    } else {
        p.sysex_overflow = 1;
    }
}

int MidiParser::feed(int byte) {
    int b = byte & 0xFF;
    // Real-time bytes stand alone, anywhere — even inside a sysex.
    if (b >= 0xF8) {
        if (b == 0xF9 || b == 0xFD) return MIDI_PARSE_NONE;   // undefined: ignore
        self.last = MidiMsg::system(b);
        return MIDI_PARSE_MESSAGE;
    }
    if (self.in_sysex) {
        if (b == MIDI_SYSEX_END) {
            midi_parser_sysex_put_(self, b);
            self.in_sysex = 0;
            if (self.sysex_overflow || self.sysex.len == 0UL) { self.sysex_overflow = 0; return MIDI_PARSE_ERROR; }
            return MIDI_PARSE_SYSEX;
        }
        if (b < 0x80) { midi_parser_sysex_put_(self, b); return MIDI_PARSE_NONE; }
        // Another status byte ends the sysex unterminated: drop it, then
        // parse the byte normally.
        self.in_sysex = 0;
        self.sysex_overflow = 0;
    }
    if (b >= 0x80) {
        if (b == MIDI_SYSEX) {
            self.in_sysex = 1;
            self.sysex_len = 0UL;
            self.sysex_overflow = 0;
            self.running = (unsigned char)0;
            midi_parser_sysex_put_(self, b);
            return MIDI_PARSE_NONE;
        }
        int len = midi_message_length(b);
        if (len < 0 || b == MIDI_SYSEX_END) {           // undefined status or a stray F7
            self.running = (unsigned char)0;
            self.have = 0;
            return MIDI_PARSE_ERROR;
        }
        // Channel messages set running status; system common clears it.
        self.running = b < 0xF0 ? (unsigned char)b : (unsigned char)0;
        self.msg[0] = (unsigned char)b;
        self.have = 1;
        self.need = len;
        if (len == 1) {
            self.last = MidiMsg::system(b);
            self.have = 0;
            return MIDI_PARSE_MESSAGE;
        }
        return MIDI_PARSE_NONE;
    }
    // A data byte.
    if (self.have == 0) {
        if (self.running == (unsigned char)0) return MIDI_PARSE_ERROR;
        self.msg[0] = self.running;
        self.have = 1;
        self.need = midi_message_length((int)self.running);
    }
    self.msg[self.have] = (unsigned char)b;
    self.have = self.have + 1;
    if (self.have >= self.need) {
        self.last = MidiMsg::of(self.msg[0..(unsigned long)self.need]);
        self.have = 0;
        return MIDI_PARSE_MESSAGE;
    }
    return MIDI_PARSE_NONE;
}

struct MidiMsg MidiParser::message() const { return self.last; }
unsigned long MidiParser::sysex_length() const { return self.sysex_len; }
[]const unsigned char MidiParser::sysex_message() const {
    unsigned long n = self.sysex_len < self.sysex.len ? self.sysex_len : self.sysex.len;
    return self.sysex[0..n];
}

// ── encoder ──────────────────────────────────────────────────────────────

struct MidiEncoder MidiEncoder::new(int use_running_status) {
    struct MidiEncoder e;
    e.running = (unsigned char)0;
    e.use_running_status = use_running_status;
    return e;
}

void MidiEncoder::reset() { self.running = (unsigned char)0; }

long MidiEncoder::encode([]const unsigned char msg, []unsigned char out) {
    unsigned long len = msg.len;
    if (len == 0UL) return 0L;
    int status = (int)msg[0];
    unsigned long skip = 0UL;
    if (status >= 0x80 && status < 0xF0) {
        if (self.use_running_status && (int)self.running == status) skip = 1UL;
        self.running = (unsigned char)status;
    } else if (status >= 0xF0 && status < 0xF8) {
        self.running = (unsigned char)0;     // system common cancels running status
    }
    unsigned long n = len - skip;
    if (n > out.len) return -1L;
    for (unsigned long i = 0UL; i < n; i = i + 1UL) out[i] = msg[i + skip];
    return (long)n;
}

// ── MidiEvent ────────────────────────────────────────────────────────────

int MidiEvent::status() const {
    if (self.bytes.len == 0UL) return 0;
    return (int)self.bytes[0];
}

struct MidiMsg MidiEvent::short_msg() const {
    struct MidiMsg m = MidiMsg::system(0);
    m.len = 0;
    if (self.bytes.len == 0UL || self.bytes.len > 3UL) return m;
    return MidiMsg::of(self.bytes);
}

// ── the input path (real-time) ───────────────────────────────────────────

double midi_in_now_(&MidiPortState_ st) {
    return (double)(audio_now_ns_() - st.t0_ns) / 1000000000.0;
}

void midi_in_reset_clock_(&MidiPortState_ st) {
    st.t0_ns = audio_now_ns_();
    st.have_last = 0;
    st.last_time = 0.0;
    st.parser.reset();
}

static int midi_filtered_(int ignore, int status) {
    if ((ignore & 1) != 0 && (status == MIDI_SYSEX)) return 1;
    if ((ignore & 2) != 0 && (status == MIDI_CLOCK || status == MIDI_TIME_CODE)) return 1;
    if ((ignore & 4) != 0 && status == MIDI_ACTIVE_SENSING) return 1;
    return 0;
}

static void midi_ring_put_(&MidiPortState_ st, unsigned long at, const &unsigned char src, unsigned long n) {
    unsafe {
        unsigned long mask = st.ring_cap - 1UL;
        for (unsigned long i = 0UL; i < n; i = i + 1UL) st.ring[(at + i) & mask] = src[i];
    }
}

static void midi_ring_get_(&MidiPortState_ st, unsigned long at, &unsigned char dst, unsigned long n) {
    unsafe {
        unsigned long mask = st.ring_cap - 1UL;
        for (unsigned long i = 0UL; i < n; i = i + 1UL) dst[i] = st.ring[(at + i) & mask];
    }
}

static void midi_count_drop_(&MidiPortState_ st) {
    unsafe { atomic_fetch_add((_Atomic unsigned long long*)&st.dropped_count, 1ULL); }
}

void midi_in_deliver_(&MidiPortState_ st, const &unsigned char msg, unsigned long len, double time) {
    if (len == 0UL) return;
    int status = 0;
    int ignore = 0;
    unsafe {
        status = (int)msg[0];
        ignore = atomic_load((_Atomic int*)&st.ignore);
    }
    if (midi_filtered_(ignore, status)) return;
    double t = time < 0.0 ? midi_in_now_(st) : time;
    double delta = st.have_last ? t - st.last_time : 0.0;
    if (delta < 0.0) delta = 0.0;
    st.last_time = t;
    st.have_last = 1;
    MidiCallbackSlot_ cb = null;
    unsafe { cb = atomic_load((MidiCallbackSlot_*)&st.callback); }
    match (cb) {
        case some(f): {
            struct MidiEvent ev;
            ev.timestamp = t;
            ev.delta = delta;
            ?&Erased user = null;
            unsafe {
                ev.bytes = ((const unsigned char*)msg)[0..len];
                user = atomic_load((MidiUserSlot_*)&st.user);
            }
            f(&ev, user);
            return;
        }
        case null: {}
    }
    // The queue: [u32 len][f64 time][len bytes].
    if (st.ring_cap == 0UL) { midi_count_drop_(st); return; }
    unsigned long need = 12UL + len;
    unsigned long head = st.ring_head;
    unsigned long tail = 0UL;
    unsafe { tail = atomic_load((_Atomic unsigned long*)&st.ring_tail); }
    if (need > st.ring_cap - (head - tail)) { midi_count_drop_(st); return; }
    unsigned int l32 = (unsigned int)len;
    unsafe {
        midi_ring_put_(st, head, (const &unsigned char)&l32, 4UL);
        midi_ring_put_(st, head + 4UL, (const &unsigned char)&t, 8UL);
        midi_ring_put_(st, head + 12UL, msg, len);
        atomic_store((_Atomic unsigned long*)&st.ring_head, head + need);
    }
}

void midi_in_feed_(&MidiPortState_ st, const &unsigned char bytes, unsigned long n, double time) {
    for (unsigned long i = 0UL; i < n; i = i + 1UL) {
        int b = 0;
        unsafe { b = (int)bytes[i]; }
        int r = st.parser.feed(b);
        if (r == MIDI_PARSE_MESSAGE) {
            struct MidiMsg m = st.parser.message();
            midi_in_deliver_(st, &m.data[0], (unsigned long)m.len, time);
        } else if (r == MIDI_PARSE_SYSEX) {
            unsafe { midi_in_deliver_(st, (const &unsigned char)st.sysex_buf, st.parser.sysex_len, time); }
        } else if (r == MIDI_PARSE_ERROR && b == MIDI_SYSEX_END) {
            midi_count_drop_(st);    // a sysex longer than MIDI_MAX_SYSEX
        }
    }
}

// ── MidiIn / MidiOut ─────────────────────────────────────────────────────

static ?&MidiPortState_ midi_state_of_(?&MidiPortState_ impl) {
    return impl;
}

static ?&static MidiBackendVt_ midi_vt_of_(&MidiPortState_ st) {
    return st.vt;
}

static void midi_state_free_(&MidiPortState_ st) {
    unsafe {
        dealloc(st.ring);
        dealloc(st.sysex_buf);
        dealloc((?&heap MidiPortState_)st);
    }
}

// Opens a client of one API; the state, or null with 'err' set.
static ?&MidiPortState_ midi_client_open_(int api, int input, const &char name, unsigned long queue, &AudioError err) {
    ?&static MidiBackendVt_ vtp = midi_backend_for_(api);
    match (vtp) {
        case null: {
            *err = AudioError::make2(AUDIO_ERR_UNSUPPORTED, "midi: API not compiled in", midi_api_name(api));
            return null;
        }
        case some(vt): {
            ?&MidiPortState_ sp = null;
            unsafe { sp = (?&MidiPortState_)alloc_zeroed_raw_(sizeof(struct MidiPortState_)); }
            match (sp) {
                case null: {}
                case some(st): {
                    st.api = api;
                    st.vt = vt;
                    st.is_input = input;
                    audio_strcpy_(&st.client_name[0], (unsigned long)MIDI_CLIENT_NAME_MAX, name);
                    if (st.client_name[0] == (char)0)
                        audio_strcpy_(&st.client_name[0], (unsigned long)MIDI_CLIENT_NAME_MAX, "SafeC");
                    st.ignore = 7;
                    int ok = 1;
                    if (input) {
                        unsigned long cap = 256UL;
                        unsigned long want = queue == 0UL ? MIDI_DEFAULT_QUEUE_BYTES : queue;
                        while (cap < want && cap < (1UL << 30)) cap = cap * 2UL;
                        unsafe {
                            st.ring = (unsigned char*)alloc_zeroed_raw_(cap);
                            st.sysex_buf = (unsigned char*)alloc_zeroed_raw_(MIDI_MAX_SYSEX);
                            if (st.ring == (unsigned char*)0 || st.sysex_buf == (unsigned char*)0) ok = 0;
                            unsafe {
                                st.parser = MidiParser::new(st.sysex_buf[0..(st.sysex_buf == (unsigned char*)0 ? 0UL : MIDI_MAX_SYSEX)]);
                            }
                        }
                        st.ring_cap = cap;
                    }
                    if (!ok) {
                        midi_state_free_(st);
                        *err = AudioError::make(AUDIO_ERR_NO_MEMORY, "midi: out of memory");
                        return null;
                    }
                    st.error = AudioError::none();
                    st.t0_ns = audio_now_ns_();
                    int rc = 0;
                    unsafe { rc = vt.client_open(st); }
                    if (rc != AUDIO_OK && rc != AUDIO_ERR_WARNING) {
                        *err = st.error;
                        midi_state_free_(st);
                        return null;
                    }
                    return (?&MidiPortState_)st;
                }
            }
            *err = AudioError::make(AUDIO_ERR_NO_MEMORY, "midi: out of memory");
            return null;
        }
    }
    return null;
}

// UNSPECIFIED: the default API, then the other available ones, then dummy.
static ?&MidiPortState_ midi_client_open_any_(int api, int input, const &char name, unsigned long queue,
                                              &int chosen, &AudioError err) {
    if (api != MIDI_API_UNSPECIFIED) {
        *chosen = api;
        return midi_client_open_(api, input, name, queue, err);
    }
    int first = midi_default_api();
    *chosen = first;
    ?&MidiPortState_ st = midi_client_open_(first, input, name, queue, err);
    if (!st.is_null()) return st;
    struct AudioError firstErr = *err;
    ?&static MidiBackendVt_ list[MIDI_API_COUNT];
    int n = midi_backends_(&list[0], MIDI_API_COUNT);
    for (int i = 0; i < n; i = i + 1) {
        int a = 0;
        match (list[i]) { case some(vt): { a = vt.api; } case null: {} }
        if (a == first || a == MIDI_API_UNSPECIFIED) continue;
        *chosen = a;
        st = midi_client_open_(a, input, name, queue, err);
        if (!st.is_null()) return st;
    }
    *err = firstErr;
    return null;
}

struct Result<MidiIn, AudioError> MidiIn::open(int api, const &char client_name, unsigned long queue_bytes) {
    struct AudioError err = AudioError::none();
    int chosen = api;
    ?&MidiPortState_ st = midi_client_open_any_(api, 1, client_name, queue_bytes, &chosen, &err);
    if (st.is_null()) return Result::new_err(err);
    struct MidiIn m;
    m.api_id = chosen;
    m.impl = st;
    return Result::new_ok(m);
}

struct Result<MidiOut, AudioError> MidiOut::open(int api, const &char client_name) {
    struct AudioError err = AudioError::none();
    int chosen = api;
    ?&MidiPortState_ st = midi_client_open_any_(api, 0, client_name, 0UL, &chosen, &err);
    if (st.is_null()) return Result::new_err(err);
    struct MidiOut m;
    m.api_id = chosen;
    m.impl = st;
    return Result::new_ok(m);
}

struct MidiIn MidiIn::closed() {
    struct MidiIn m;
    m.api_id = MIDI_API_UNSPECIFIED;
    m.impl = null;
    return m;
}

struct MidiOut MidiOut::closed() {
    struct MidiOut m;
    m.api_id = MIDI_API_UNSPECIFIED;
    m.impl = null;
    return m;
}

static unsigned int midi_port_count_(?&MidiPortState_ impl) {
    match (midi_state_of_(impl)) {
        case some(st): {
            match (midi_vt_of_(st)) {
                case some(vt): {
                    unsigned int n = 0U;
                    unsafe { n = vt.port_count(st); }
                    return n;
                }
                case null: {}
            }
        }
        case null: {}
    }
    return 0U;
}

static struct Result<MidiPortInfo, AudioError> midi_port_info_(?&MidiPortState_ impl, unsigned int index) {
    match (midi_state_of_(impl)) {
        case some(st): {
            match (midi_vt_of_(st)) {
                case some(vt): {
                    struct MidiPortInfo info;
                    info.index = index;
                    info.name[0] = (char)0;
                    int rc = 0;
                    unsafe { rc = vt.port_name(st, index, &info.name[0], (unsigned long)MIDI_PORT_NAME_MAX); }
                    if (rc != AUDIO_OK) return Result::new_err(AudioError::make(AUDIO_ERR_INVALID_DEVICE, "midi: no port with that index"));
                    return Result::new_ok(info);
                }
                case null: {}
            }
        }
        case null: {}
    }
    return Result::new_err(AudioError::make(AUDIO_ERR_INVALID_USE, "midi: not open"));
}

static struct AudioError midi_open_port_(?&MidiPortState_ impl, int isVirtual, unsigned int index, const &char name) {
    match (midi_state_of_(impl)) {
        case some(st): {
            if (st.port_open) return AudioError::make(AUDIO_ERR_INVALID_USE, "midi: a port is already open (close_port() first)");
            match (midi_vt_of_(st)) {
                case some(vt): {
                    char pname[MIDI_PORT_NAME_MAX];
                    audio_strcpy_(&pname[0], (unsigned long)MIDI_PORT_NAME_MAX, name);
                    if (pname[0] == (char)0)
                        audio_strcpy_(&pname[0], (unsigned long)MIDI_PORT_NAME_MAX, st.is_input ? "input" : "output");
                    if (st.is_input) midi_in_reset_clock_(st);
                    st.error = AudioError::none();
                    int rc = 0;
                    unsafe { rc = isVirtual ? vt.open_virtual(st, &pname[0]) : vt.open_port(st, index, &pname[0]); }
                    if (rc != AUDIO_OK && rc != AUDIO_ERR_WARNING) return st.error;
                    st.port_open = 1;
                    return AudioError::none();
                }
                case null: {}
            }
        }
        case null: {}
    }
    return AudioError::make(AUDIO_ERR_INVALID_USE, "midi: not open");
}

static void midi_close_port_(?&MidiPortState_ impl) {
    match (midi_state_of_(impl)) {
        case some(st): {
            if (!st.port_open) return;
            match (midi_vt_of_(st)) {
                case some(vt): { unsafe { vt.close_port(st); } }
                case null: {}
            }
            st.port_open = 0;
        }
        case null: {}
    }
}

static void midi_close_(?&MidiPortState_ impl) {
    midi_close_port_(impl);
    match (midi_state_of_(impl)) {
        case some(st): {
            match (midi_vt_of_(st)) {
                case some(vt): { unsafe { vt.client_close(st); } }
                case null: {}
            }
            midi_state_free_(st);
        }
        case null: {}
    }
}

int MidiIn::is_open() const { return !self.impl.is_null(); }
int MidiIn::api() const { return self.api_id; }
unsigned int MidiIn::port_count() { return midi_port_count_(self.impl); }
struct Result<MidiPortInfo, AudioError> MidiIn::port_info(unsigned int index) { return midi_port_info_(self.impl, index); }
struct AudioError MidiIn::open_port(unsigned int index, const &char port_name) {
    return midi_open_port_(self.impl, 0, index, port_name);
}
struct AudioError MidiIn::open_virtual_port(const &char port_name) {
    return midi_open_port_(self.impl, 1, 0U, port_name);
}
void MidiIn::close_port() { midi_close_port_(self.impl); }
int MidiIn::is_port_open() const {
    match (midi_state_of_(self.impl)) { case some(st): return st.port_open; case null: {} }
    return 0;
}

void MidiIn::set_callback_raw_(MidiCallbackRaw_ callback, ?&Erased user) {
    match (midi_state_of_(self.impl)) {
        case some(st): {
            unsafe {
                atomic_store((MidiCallbackSlot_*)&st.callback, (MidiCallbackSlot_)null);
                atomic_store((MidiUserSlot_*)&st.user, user);
                atomic_store((MidiCallbackSlot_*)&st.callback, (MidiCallbackSlot_)callback);
            }
        }
        case null: {}
    }
}

void MidiIn::cancel_callback() {
    match (midi_state_of_(self.impl)) {
        case some(st): { unsafe { atomic_store((MidiCallbackSlot_*)&st.callback, (MidiCallbackSlot_)null); } }
        case null: {}
    }
}

void MidiIn::ignore_types(int sysex, int timing, int active_sensing) {
    int v = (sysex ? 1 : 0) | (timing ? 2 : 0) | (active_sensing ? 4 : 0);
    match (midi_state_of_(self.impl)) {
        case some(st): { unsafe { atomic_store((_Atomic int*)&st.ignore, v); } }
        case null: {}
    }
}

long MidiIn::get_message([]unsigned char buf, &double timestamp) {
    match (midi_state_of_(self.impl)) {
        case some(st): {
            if (st.ring_cap == 0UL) return 0L;
            unsigned long tail = st.ring_tail;
            unsigned long head = 0UL;
            unsafe { head = atomic_load((_Atomic unsigned long*)&st.ring_head); }
            if (head == tail) return 0L;
            unsigned int l32 = 0U;
            double t = 0.0;
            unsafe {
                midi_ring_get_(st, tail, (&unsigned char)&l32, 4UL);
                midi_ring_get_(st, tail + 4UL, (&unsigned char)&t, 8UL);
            }
            unsigned long len = (unsigned long)l32;
            if (len > buf.len) return 0L - (long)len;
            if (len > 0UL) unsafe { midi_ring_get_(st, tail + 12UL, (&unsigned char)&buf[0], len); }
            *timestamp = t;
            unsafe { atomic_store((_Atomic unsigned long*)&st.ring_tail, tail + 12UL + len); }
            return (long)len;
        }
        case null: {}
    }
    return 0L;
}

unsigned long MidiIn::dropped() const {
    match (midi_state_of_(self.impl)) {
        case some(st): {
            unsigned long long d = 0ULL;
            unsafe { d = atomic_load((_Atomic unsigned long long*)&st.dropped_count); }
            return (unsigned long)d;
        }
        case null: {}
    }
    return 0UL;
}

void MidiIn::close() { self.drop(); }

void MidiIn::drop() {
    midi_close_(self.impl);
    self.impl = null;
    self.api_id = MIDI_API_UNSPECIFIED;
}

int MidiOut::is_open() const { return !self.impl.is_null(); }
int MidiOut::api() const { return self.api_id; }
unsigned int MidiOut::port_count() { return midi_port_count_(self.impl); }
struct Result<MidiPortInfo, AudioError> MidiOut::port_info(unsigned int index) { return midi_port_info_(self.impl, index); }
struct AudioError MidiOut::open_port(unsigned int index, const &char port_name) {
    return midi_open_port_(self.impl, 0, index, port_name);
}
struct AudioError MidiOut::open_virtual_port(const &char port_name) {
    return midi_open_port_(self.impl, 1, 0U, port_name);
}
void MidiOut::close_port() { midi_close_port_(self.impl); }
int MidiOut::is_port_open() const {
    match (midi_state_of_(self.impl)) { case some(st): return st.port_open; case null: {} }
    return 0;
}

struct AudioError MidiOut::send_message([]const unsigned char msg) {
    unsigned long len = msg.len;
    match (midi_state_of_(self.impl)) {
        case some(st): {
            if (!st.port_open) return AudioError::make(AUDIO_ERR_INVALID_USE, "midi: no port open");
            if (len == 0UL) return AudioError::make(AUDIO_ERR_INVALID_PARAM, "midi: empty message");
            int first = (int)msg[0];
            if (first < 0x80) return AudioError::make(AUDIO_ERR_INVALID_PARAM, "midi: a message starts with a status byte");
            match (midi_vt_of_(st)) {
                case some(vt): {
                    st.error = AudioError::none();
                    int rc = 0;
                    unsafe { rc = vt.send(st, (const &unsigned char)&msg[0], len); }
                    if (rc != AUDIO_OK) return st.error;
                    return AudioError::none();
                }
                case null: {}
            }
        }
        case null: {}
    }
    return AudioError::make(AUDIO_ERR_INVALID_USE, "midi: not open");
}

struct AudioError MidiOut::send(struct MidiMsg msg) {
    if (msg.len <= 0 || msg.len > 3) return AudioError::make(AUDIO_ERR_INVALID_PARAM, "midi: empty message");
    return self.send_message(msg.bytes());
}

void MidiOut::close() { self.drop(); }

void MidiOut::drop() {
    midi_close_(self.impl);
    self.impl = null;
    self.api_id = MIDI_API_UNSPECIFIED;
}

} // namespace std