std1.0.0-beta-7 SafeC docs

Source audio/audio.sc

View in the repository

// SafeC Standard Library — real-time audio I/O: the API-independent core
// (see audio.h): the API registry, device lists, stream setup, the
// real-time conversion/callback step every backend calls, stream control.
// safeguard: implements <std/audio/audio_backend.h>
#pragma once
#include <std/audio/audio.h>
#include <std/audio/audio_backend.h>
#include <std/audio/audio_convert.h>
#include <std/audio/audio_sys.h>
#include <std/mem.h>
#include <std/thread.h>

namespace std {

// ── formats & API names ──────────────────────────────────────────────────

int audio_format_bytes(unsigned int format) {
    if (format == AUDIO_FORMAT_I8) return 1;
    if (format == AUDIO_FORMAT_I16) return 2;
    if (format == AUDIO_FORMAT_I24) return 3;
    if (format == AUDIO_FORMAT_I32) return 4;
    if (format == AUDIO_FORMAT_F32) return 4;
    if (format == AUDIO_FORMAT_F64) return 8;
    return 0;
}

const &char audio_format_name(unsigned int format) {
    if (format == AUDIO_FORMAT_I8) return "i8";
    if (format == AUDIO_FORMAT_I16) return "i16";
    if (format == AUDIO_FORMAT_I24) return "i24";
    if (format == AUDIO_FORMAT_I32) return "i32";
    if (format == AUDIO_FORMAT_F32) return "f32";
    if (format == AUDIO_FORMAT_F64) return "f64";
    return "";
}

const &char audio_api_name(int api) {
    if (api == AUDIO_API_ALSA) return "alsa";
    if (api == AUDIO_API_JACK) return "jack";
    if (api == AUDIO_API_COREAUDIO) return "coreaudio";
    if (api == AUDIO_API_WASAPI) return "wasapi";
    if (api == AUDIO_API_WEBAUDIO) return "webaudio";
    if (api == AUDIO_API_DUMMY) return "dummy";
    return "";
}

const &char audio_api_display_name(int api) {
    if (api == AUDIO_API_ALSA) return "ALSA";
    if (api == AUDIO_API_JACK) return "JACK";
    if (api == AUDIO_API_COREAUDIO) return "Core Audio";
    if (api == AUDIO_API_WASAPI) return "WASAPI";
    if (api == AUDIO_API_WEBAUDIO) return "Web Audio";
    if (api == AUDIO_API_DUMMY) return "Dummy";
    return "";
}

int audio_api_by_name(const &char name) {
    for (int a = 1; a < AUDIO_API_COUNT; a = a + 1)
        if (audio_streq_(name, audio_api_name(a))) return a;
    return AUDIO_API_UNSPECIFIED;
}

// ── the registry ─────────────────────────────────────────────────────────

// The compiled backends, in preference order.
static int audio_backends_(&?&static AudioBackendVt_ out, int cap) {
    int n = 0;
#if defined(__APPLE__)
    if (n < cap) { unsafe { out[n] = (?&static AudioBackendVt_)audio_coreaudio_backend_(); } n = n + 1; }
    if (n < cap) { unsafe { out[n] = (?&static AudioBackendVt_)audio_jack_backend_(); } n = n + 1; }
#endif
#if defined(__linux__)
    if (n < cap) { unsafe { out[n] = (?&static AudioBackendVt_)audio_alsa_backend_(); } n = n + 1; }
    if (n < cap) { unsafe { out[n] = (?&static AudioBackendVt_)audio_jack_backend_(); } n = n + 1; }
#endif
#if defined(_WIN32)
    if (n < cap) { unsafe { out[n] = (?&static AudioBackendVt_)audio_wasapi_backend_(); } n = n + 1; }
#endif
#if defined(__wasm__)
    if (n < cap) { unsafe { out[n] = (?&static AudioBackendVt_)audio_webaudio_backend_(); } n = n + 1; }
#endif
    if (n < cap) { unsafe { out[n] = (?&static AudioBackendVt_)audio_dummy_backend_(); } n = n + 1; }
    return n;
}

static ?&static AudioBackendVt_ audio_backend_for_(int api) {
    ?&static AudioBackendVt_ list[AUDIO_API_COUNT];
    int n = audio_backends_(&list[0], AUDIO_API_COUNT);
    for (int i = 0; i < n; i = i + 1) {
        match (list[i]) {
            case some(vt): { if (vt.api == api) return (?&static AudioBackendVt_)vt; }
            case null: {}
        }
    }
    return null;
}

int audio_compiled_apis([]int out) {
    ?&static AudioBackendVt_ list[AUDIO_API_COUNT];
    int n = audio_backends_(&list[0], AUDIO_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 audio_api_compiled(int api) { return !audio_backend_for_(api).is_null(); }

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

int audio_default_api() {
    ?&char env = audio_getenv_("SAFEC_AUDIO_API");
    match (env) {
        case some(e): {
            int a = audio_api_by_name(e);
            if (a != AUDIO_API_UNSPECIFIED && audio_api_compiled(a)) return a;
        }
        case null: {}
    }
    ?&static AudioBackendVt_ list[AUDIO_API_COUNT];
    int n = audio_backends_(&list[0], AUDIO_API_COUNT);
    // JACK only when its server is up (it's a choice the user made then).
    for (int i = 0; i < n; i = i + 1) {
        match (list[i]) {
            case some(vt): {
                if (vt.api == AUDIO_API_JACK && audio_backend_available_(vt)) return AUDIO_API_JACK;
            }
            case null: {}
        }
    }
    for (int i = 0; i < n; i = i + 1) {
        match (list[i]) {
            case some(vt): {
                if (vt.api != AUDIO_API_JACK && vt.api != AUDIO_API_DUMMY) return vt.api;
            }
            case null: {}
        }
    }
    return AUDIO_API_DUMMY;
}

// ── device lists ─────────────────────────────────────────────────────────

int AudioDeviceInfo::supports_rate(unsigned int rate) const {
    for (int i = 0; i < self.sample_rate_count && i < AUDIO_MAX_SAMPLE_RATES; i = i + 1)
        if (self.sample_rates[i] == rate) return 1;
    return 0;
}

struct AudioDeviceInfo AudioDeviceInfo::none() {
    struct AudioDeviceInfo d;
    safe_memset_obj(&d, 0);
    return d;
}

struct AudioDeviceInfo audio_device_info_blank_() { return AudioDeviceInfo::none(); }

void audio_device_add_rate_(&AudioDeviceInfo info, unsigned int rate) {
    int n = info.sample_rate_count;
    for (int i = 0; i < n; i = i + 1) if (info.sample_rates[i] == rate) return;
    if (n >= AUDIO_MAX_SAMPLE_RATES) return;
    int at = n;
    while (at > 0 && info.sample_rates[at - 1] > rate) {
        info.sample_rates[at] = info.sample_rates[at - 1];
        at = at - 1;
    }
    info.sample_rates[at] = rate;
    info.sample_rate_count = n + 1;
}

static const unsigned int kAudioStdRates_[14] = {
    4000U, 5512U, 8000U, 9600U, 11025U, 16000U, 22050U, 32000U, 44100U, 48000U, 88200U, 96000U,
    176400U, 192000U
};

int audio_standard_rate_count_() { return 14; }
unsigned int audio_standard_rate_(int i) {
    if (i >= 0 && i < 14) return kAudioStdRates_[i];
    return 0U;
}


void audio_host_clear_(&AudioHost_ h) {
    h.count = 0U;
    h.default_out = AUDIO_DEFAULT_DEVICE;
    h.default_in = AUDIO_DEFAULT_DEVICE;
}

static unsigned int audio_host_id_for_(&AudioHost_ h, const &char key) {
    unsafe {
        for (unsigned int i = 0U; i < h.known_count; i = i + 1U)
            if (audio_streq_(&h.known[i].key[0], key)) return h.known[i].id;
        if (h.known_count >= h.known_cap) {
            unsigned int ncap = h.known_cap == 0U ? 8U : h.known_cap * 2U;
            ?&heap AudioKnownId_ nk = realloc_buf((?&heap AudioKnownId_)h.known,
                                                  checked_mul_size((unsigned long)ncap, sizeof(struct AudioKnownId_)));
            if (nk.is_null()) return 0U;
            h.known = (struct AudioKnownId_*)nk;
            h.known_cap = ncap;
        }
        h.next_id = h.next_id + 1U;
        struct AudioKnownId_* k = &h.known[h.known_count];
        audio_strcpy_(&k->key[0], (unsigned long)AUDIO_DEVICE_KEY_MAX, key);
        k->id = h.next_id;
        h.known_count = h.known_count + 1U;
        return h.next_id;
    }
}

?&AudioDeviceRec_ audio_host_add_(&AudioHost_ h, &AudioDeviceInfo info, const &char key, long long native) {
    unsigned int id = audio_host_id_for_(h, key);
    if (id == 0U) return null;
    unsafe {
        if (h.count >= h.cap) {
            unsigned int ncap = h.cap == 0U ? 8U : h.cap * 2U;
            ?&heap AudioDeviceRec_ nd = realloc_buf((?&heap AudioDeviceRec_)h.devs,
                                                    checked_mul_size((unsigned long)ncap, sizeof(struct AudioDeviceRec_)));
            if (nd.is_null()) return null;
            h.devs = (struct AudioDeviceRec_*)nd;
            h.cap = ncap;
        }
        struct AudioDeviceRec_* r = &h.devs[h.count];
        r->info = *info;
        r->info.id = id;
        audio_strcpy_(&r->key[0], (unsigned long)AUDIO_DEVICE_KEY_MAX, key);
        r->native = native;
        h.count = h.count + 1U;
        return (?&AudioDeviceRec_)r;
    }
}

static ?&AudioDeviceRec_ audio_host_find_(&AudioHost_ h, unsigned int id) {
    unsafe {
        for (unsigned int i = 0U; i < h.count; i = i + 1U)
            if (h.devs[i].info.id == id) return (?&AudioDeviceRec_)&h.devs[i];
    }
    return null;
}

// ── Audio ────────────────────────────────────────────────────────────────

struct Audio Audio::closed() {
    struct Audio a;
    a.api_id = AUDIO_API_UNSPECIFIED;
    a.impl = null;
    return a;
}

static ?&AudioHost_ audio_host_of_(const &Audio a) {
    return a.impl;
}

static int audio_host_probe_(&AudioHost_ h) {
    ?&static AudioBackendVt_ vt = audio_backend_for_(h.api);
    match (vt) {
        case some(v): {
            audio_host_clear_(h);
            h.error = AudioError::none();
            int rc = 0;
            unsafe { rc = v.probe(h); }
            return rc;
        }
        case null: {}
    }
    return audio_fail_(&h.error, AUDIO_ERR_UNSUPPORTED, "audio API not compiled in");
}

static struct Result<Audio, AudioError> audio_open_api_(int api) {
    ?&static AudioBackendVt_ vt = audio_backend_for_(api);
    if (vt.is_null()) {
        struct AudioError e = AudioError::make2(AUDIO_ERR_UNSUPPORTED, "audio API not compiled in",
                                                audio_api_name(api));
        return Result::new_err(e);
    }
    ?&AudioHost_ hp = null;
    unsafe { hp = (?&AudioHost_)alloc_zeroed_raw_(sizeof(struct AudioHost_)); }
    match (hp) {
        case some(h): {
            h.api = api;
            int rc = audio_host_probe_(h);
            if (rc != AUDIO_OK && rc != AUDIO_ERR_WARNING) {
                struct AudioError e = h.error;
                unsafe {
                    match (vt) { case some(v): { v.host_close(h); } case null: {} }
                    dealloc((?&heap AudioDeviceRec_)h.devs);
                    dealloc((?&heap AudioKnownId_)h.known);
                    dealloc((?&heap AudioHost_)h);
                }
                return Result::new_err(e);
            }
            struct Audio a;
            a.api_id = api;
            a.impl = h;
            return Result::new_ok(a);
        }
        case null: {}
    }
    return Result::new_err(AudioError::make(AUDIO_ERR_NO_MEMORY, "out of memory"));
}

struct Result<Audio, AudioError> Audio::open(int api) {
    if (api != AUDIO_API_UNSPECIFIED) return audio_open_api_(api);
    // An explicit SAFEC_AUDIO_API wins.
    ?&char env = audio_getenv_("SAFEC_AUDIO_API");
    match (env) {
        case some(e): {
            int a = audio_api_by_name(e);
            if (a != AUDIO_API_UNSPECIFIED && audio_api_compiled(a)) return audio_open_api_(a);
        }
        case null: {}
    }
    // The default API, then the first other one with devices, then the
    // first that opened at all (no devices), then dummy.
    int first = audio_default_api();
    struct Result<Audio, AudioError> r = audio_open_api_(first);
    int firstHasDevices = 0;
    match (r.get_ok()) { case some(a): { firstHasDevices = a.device_count() > 0U; } case null: {} }
    if (firstHasDevices) return r;
    ?&static AudioBackendVt_ list[AUDIO_API_COUNT];
    int n = audio_backends_(&list[0], AUDIO_API_COUNT);
    for (int i = 0; i < n; i = i + 1) {
        int api2 = 0;
        int avail = 0;
        match (list[i]) {
            case some(vt): { api2 = vt.api; avail = audio_backend_available_(vt); }
            case null: {}
        }
        if (api2 == first || api2 == AUDIO_API_DUMMY || !avail) continue;
        struct Result<Audio, AudioError> r2 = audio_open_api_(api2);
        int hasDevices = 0;
        match (r2.get_ok()) { case some(a2): { hasDevices = a2.device_count() > 0U; } case null: {} }
        if (hasDevices) {
            r.drop();   // the first API's Audio (opened, no devices)
            return r2;
        }
        r2.drop();
    }
    if (r.ok()) return r;
    r.drop();
    return audio_open_api_(AUDIO_API_DUMMY);
}

int Audio::is_open() const { return !self.impl.is_null(); }
int Audio::api() const { return self.api_id; }

struct AudioError Audio::rescan() {
    ?&AudioHost_ hp = audio_host_of_(self);
    match (hp) {
        case some(h): {
            int rc = audio_host_probe_(h);
            if (rc != AUDIO_OK) return h.error;
            return AudioError::none();
        }
        case null: {}
    }
    return AudioError::make(AUDIO_ERR_INVALID_USE, "audio: not open");
}

unsigned int Audio::device_count() const {
    ?&AudioHost_ hp = audio_host_of_(self);
    match (hp) { case some(h): return h.count; case null: {} }
    return 0U;
}

struct Result<AudioDeviceInfo, AudioError> Audio::device_info(unsigned int index) const {
    ?&AudioHost_ hp = audio_host_of_(self);
    match (hp) {
        case some(h): {
            if (index < h.count) {
                struct AudioDeviceInfo info;
                unsafe { info = h.devs[index].info; }
                return Result::new_ok(info);
            }
            return Result::new_err(AudioError::make(AUDIO_ERR_INVALID_DEVICE, "audio: device index out of range"));
        }
        case null: {}
    }
    return Result::new_err(AudioError::make(AUDIO_ERR_INVALID_USE, "audio: not open"));
}

struct Result<AudioDeviceInfo, AudioError> Audio::device_by_id(unsigned int id) const {
    ?&AudioHost_ hp = audio_host_of_(self);
    match (hp) {
        case some(h): {
            match (audio_host_find_(h, id)) {
                case some(r): return Result::new_ok(r.info);
                case null: {}
            }
            return Result::new_err(AudioError::make(AUDIO_ERR_INVALID_DEVICE, "audio: no device with that id"));
        }
        case null: {}
    }
    return Result::new_err(AudioError::make(AUDIO_ERR_INVALID_USE, "audio: not open"));
}

unsigned int Audio::default_output_device() const {
    ?&AudioHost_ hp = audio_host_of_(self);
    match (hp) { case some(h): return h.default_out; case null: {} }
    return AUDIO_DEFAULT_DEVICE;
}

unsigned int Audio::default_input_device() const {
    ?&AudioHost_ hp = audio_host_of_(self);
    match (hp) { case some(h): return h.default_in; case null: {} }
    return AUDIO_DEFAULT_DEVICE;
}

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

void Audio::drop() {
    ?&AudioHost_ hp = audio_host_of_(self);
    match (hp) {
        case some(h): {
            match (audio_backend_for_(h.api)) {
                case some(v): { unsafe { v.host_close(h); } }
                case null: {}
            }
            unsafe {
                dealloc((?&heap AudioDeviceRec_)h.devs);
                dealloc((?&heap AudioKnownId_)h.known);
                dealloc((?&heap AudioHost_)h);
            }
        }
        case null: {}
    }
    self.impl = null;
    self.api_id = AUDIO_API_UNSPECIFIED;
}

// ── parameters ───────────────────────────────────────────────────────────

struct AudioStreamParams AudioStreamParams::of(unsigned int device, int channels) {
    struct AudioStreamParams p;
    p.device = device;
    p.channels = channels;
    p.first_channel = 0;
    return p;
}

struct AudioStreamOptions AudioStreamOptions::none() {
    struct AudioStreamOptions o;
    safe_memset_obj(&o, 0);
    return o;
}

struct AudioStreamOptions AudioStreamOptions::with_flags(unsigned int flags) {
    struct AudioStreamOptions o = AudioStreamOptions::none();
    o.flags = flags;
    return o;
}

// ── opening a stream ─────────────────────────────────────────────────────

static int audio_format_valid_(unsigned int f) { return audio_format_bytes(f) != 0; }

static struct Result<AudioStream, AudioError> audio_stream_err_(int code, const &char msg) {
    return Result::new_err(AudioError::make(code, msg));
}

// Resolves one direction's device against the host's list.
static int audio_resolve_dir_(&AudioHost_ h, &AudioStreamState_ st, int dir, &AudioStreamParams p, &AudioError err) {
    if (p.channels < 1) return audio_fail_(err, AUDIO_ERR_INVALID_PARAM, "audio: channels must be >= 1");
    if (p.first_channel < 0) return audio_fail_(err, AUDIO_ERR_INVALID_PARAM, "audio: first_channel must be >= 0");
    st.user_channels[dir] = p.channels;
    st.first_channel[dir] = p.first_channel;
    if (st.device_name[0] != (char)0) {
        st.device_id[dir] = p.device;      // the backend opens device_name; the id is informational
        return AUDIO_OK;
    }
    unsigned int id = p.device;
    if (id == AUDIO_DEFAULT_DEVICE) id = dir == AUDIO_DIR_OUT ? h.default_out : h.default_in;
    if (id == AUDIO_DEFAULT_DEVICE) {
        // No default: the first device that has channels in this direction.
        unsafe {
            for (unsigned int i = 0U; i < h.count; i = i + 1U) {
                int ch = dir == AUDIO_DIR_OUT ? h.devs[i].info.output_channels : h.devs[i].info.input_channels;
                if (ch > 0) { id = h.devs[i].info.id; break; }
            }
        }
    }
    if (id == AUDIO_DEFAULT_DEVICE)
        return audio_fail_(err, AUDIO_ERR_NO_DEVICES,
                           dir == AUDIO_DIR_OUT ? "audio: no output device" : "audio: no input device");
    match (audio_host_find_(h, id)) {
        case some(r): {
            int have = dir == AUDIO_DIR_OUT ? r.info.output_channels : r.info.input_channels;
            if (p.channels + p.first_channel > have)
                return audio_fail2_(err, AUDIO_ERR_INVALID_PARAM, "audio: device has too few channels",
                                    &r.info.name[0]);
            st.device_id[dir] = id;
            audio_strcpy_(&st.device_key[dir][0], (unsigned long)AUDIO_DEVICE_KEY_MAX, &r.key[0]);
            st.device_native[dir] = r.native;
            return AUDIO_OK;
        }
        case null: {}
    }
    return audio_fail_(err, AUDIO_ERR_INVALID_DEVICE, "audio: no device with that id");
}

// An empty buffer (an unused direction's).
static []unsigned char audio_no_bytes_() {
    unsafe { return ((unsigned char*)0)[0..0]; }
}

// A zeroed heap buffer of 'n' bytes (at least 16); empty on OOM.
static []unsigned char audio_alloc_bytes_(unsigned long n) {
    unsigned long len = n == 0UL ? 16UL : n;
    ?&heap unsigned char p = alloc_zeroed(len);
    match (p) {
        case some(b): { unsafe { return ((unsigned char*)b)[0..len]; } }
        case null: {}
    }
    return audio_no_bytes_();
}

static void audio_stream_free_buffers_(&AudioStreamState_ st) {
    for (int d = 0; d < 2; d = d + 1) {
        unsafe {
            unsigned char* db = st.device_buf[d].ptr;
            unsigned char* ub = st.user_buf[d].ptr;
            if (db != ub) dealloc((?&heap unsigned char)db);
            dealloc((?&heap unsigned char)ub);
        }
        st.device_buf[d] = audio_no_bytes_();
        st.user_buf[d] = audio_no_bytes_();
    }
}

// After the backend's open(): decide per direction whether the callback's
// layout differs from the device's, and allocate both buffers.
static int audio_stream_alloc_buffers_(&AudioStreamState_ st) {
    if (st.max_frames < st.buffer_frames) st.max_frames = st.buffer_frames;
    for (int d = 0; d < 2; d = d + 1) {
        int on = d == AUDIO_DIR_OUT ? st.mode_out : st.mode_in;
        if (!on) continue;
        if (st.device_channels[d] == 0) st.device_channels[d] = st.user_channels[d] + st.first_channel[d];
        if (st.device_format[d] == 0U) st.device_format[d] = st.user_format;
        unsigned long frames = (unsigned long)st.max_frames;
        unsigned long ub = frames * (unsigned long)st.user_channels[d] * (unsigned long)audio_format_bytes(st.user_format);
        unsigned long db = frames * (unsigned long)st.device_channels[d] *
                           (unsigned long)audio_format_bytes(st.device_format[d]);
        int same = st.device_format[d] == st.user_format && st.device_channels[d] == st.user_channels[d] &&
                   st.first_channel[d] == 0 &&
                   (st.device_interleaved[d] == st.user_interleaved || st.user_channels[d] == 1);
        st.convert[d] = !same;
        st.user_buf[d] = audio_alloc_bytes_(ub);
        if (st.user_buf[d].len == 0UL) return AUDIO_ERR_NO_MEMORY;
        if (same) st.device_buf[d] = st.user_buf[d];
        else {
            st.device_buf[d] = audio_alloc_bytes_(db);
            if (st.device_buf[d].len == 0UL) return AUDIO_ERR_NO_MEMORY;
        }
    }
    return AUDIO_OK;
}

static ?&static AudioBackendVt_ audio_stream_vt_(&AudioStreamState_ st) {
    return st.vt;
}

struct Result<AudioStream, AudioError> Audio::open_stream_raw_(?&AudioStreamParams output, ?&AudioStreamParams input,
                                                               unsigned int format, unsigned int sample_rate,
                                                               &unsigned int frames, AudioCallbackRaw_ callback,
                                                               ?&Erased user, ?&AudioStreamOptions opts) {
    ?&AudioHost_ hp = audio_host_of_(self);
    if (hp.is_null()) return audio_stream_err_(AUDIO_ERR_INVALID_USE, "audio: not open");
    if (output.is_null() && input.is_null())
        return audio_stream_err_(AUDIO_ERR_INVALID_PARAM, "audio: a stream needs output and/or input parameters");
    if (!audio_format_valid_(format)) return audio_stream_err_(AUDIO_ERR_INVALID_PARAM, "audio: invalid sample format");
    if (sample_rate == 0U) return audio_stream_err_(AUDIO_ERR_INVALID_PARAM, "audio: sample rate must be > 0");
    ?&static AudioBackendVt_ vtp = audio_backend_for_(self.api_id);
    if (vtp.is_null()) return audio_stream_err_(AUDIO_ERR_UNSUPPORTED, "audio: API not compiled in");

    ?&AudioStreamState_ sp = null;
    unsafe { sp = (?&AudioStreamState_)alloc_zeroed_raw_(sizeof(struct AudioStreamState_)); }
    match (sp) {
        case null: { return audio_stream_err_(AUDIO_ERR_NO_MEMORY, "audio: out of memory"); }
        case some(st): {
            match (hp) {
                case null: {}
                case some(h): {
                    match (vtp) {
                        case null: {}
                        case some(vt): {
                            st.api = self.api_id;
                            st.vt = vt;
                            st.user_format = format;
                            st.sample_rate = sample_rate;
                            st.buffer_frames = *frames;
                            st.callback = callback;
                            st.user = user;
                            st.state = AUDIO_STREAM_CLOSED;
                            st.user_interleaved = 1;
                            match (opts) {
                                case some(o): {
                                    st.flags = o.flags;
                                    st.buffer_count = o.buffer_count;
                                    st.priority = o.priority;
                                    audio_strcpy_(&st.stream_name[0], (unsigned long)AUDIO_STREAM_NAME_MAX, &o.stream_name[0]);
                                    audio_strcpy_(&st.device_name[0], (unsigned long)AUDIO_DEVICE_NAME_MAX, &o.device_name[0]);
                                }
                                case null: {}
                            }
                            if ((st.flags & AUDIO_FLAG_NONINTERLEAVED) != 0U) st.user_interleaved = 0;
                            if (st.stream_name[0] == (char)0)
                                audio_strcpy_(&st.stream_name[0], (unsigned long)AUDIO_STREAM_NAME_MAX, "SafeC");
                            struct AudioError err = AudioError::none();
                            int rc = AUDIO_OK;
                            match (output) {
                                case some(p): { st.mode_out = 1; rc = audio_resolve_dir_(h, st, AUDIO_DIR_OUT, p, &err); }
                                case null: {}
                            }
                            if (rc == AUDIO_OK) {
                                match (input) {
                                    case some(p): { st.mode_in = 1; rc = audio_resolve_dir_(h, st, AUDIO_DIR_IN, p, &err); }
                                    case null: {}
                                }
                            }
                            if (rc == AUDIO_OK) {
                                st.error = AudioError::none();
                                unsafe { rc = vt.open(h, st); }
                                if (rc != AUDIO_OK && rc != AUDIO_ERR_WARNING) err = st.error;
                                else {
                                    rc = audio_stream_alloc_buffers_(st);
                                    if (rc != AUDIO_OK) {
                                        err = AudioError::make(rc, "audio: out of memory");
                                        unsafe { vt.close(st); }
                                    }
                                }
                            }
                            if (rc != AUDIO_OK && rc != AUDIO_ERR_WARNING) {
                                audio_stream_free_buffers_(st);
                                unsafe { dealloc((?&heap AudioStreamState_)st); }
                                return Result::new_err(err);
                            }
                            *frames = st.buffer_frames;
                            unsafe { atomic_store((_Atomic int*)&st.state, AUDIO_STREAM_STOPPED); }
                            struct AudioStream s;
                            s.impl = st;
                            return Result::new_ok(s);
                        }
                    }
                }
            }
            unsafe { dealloc((?&heap AudioStreamState_)st); }
        }
    }
    return audio_stream_err_(AUDIO_ERR_INVALID_USE, "audio: not open");
}

// ── the real-time step ───────────────────────────────────────────────────

unsigned long audio_stream_device_frame_bytes_(&AudioStreamState_ st, int dir) {
    return (unsigned long)st.device_channels[dir] * (unsigned long)audio_format_bytes(st.device_format[dir]);
}

void audio_stream_flag_status_(&AudioStreamState_ st, unsigned int status) {
    unsafe { atomic_fetch_or((_Atomic unsigned int*)&st.pending_status, status); }
}

int audio_stream_stop_requested_(&AudioStreamState_ st) {
    unsafe { return atomic_load((_Atomic int*)&st.stop_request); }
}

void audio_stream_request_stop_(&AudioStreamState_ st, int how) {
    unsafe {
        // ABORT overrides STOP, never the other way round.
        int cur = atomic_load((_Atomic int*)&st.stop_request);
        if (how > cur) atomic_store((_Atomic int*)&st.stop_request, how);
    }
}

void audio_stream_mark_stopped_(&AudioStreamState_ st, ?&AudioError err) {
    match (err) {
        case some(e): { if (e.failed()) audio_stream_set_rt_error_(st, e.code, &e.message[0]); }
        case null: {}
    }
    unsafe { atomic_store((_Atomic int*)&st.state, AUDIO_STREAM_STOPPED); }
}

void audio_stream_set_rt_error_(&AudioStreamState_ st, int code, const &char message) {
    unsafe {
        int expected = 0;
        // Claim the slot first (2 = being written), then publish (1).
        if (atomic_compare_exchange_strong((_Atomic int*)&st.rt_error_set, &expected, 2)) {
            st.rt_error.code = code;
            audio_strcpy_(&st.rt_error.message[0], (unsigned long)AUDIO_ERROR_MESSAGE_MAX, message);
            atomic_store((_Atomic int*)&st.rt_error_set, 1);
        }
    }
}

void audio_stream_silence_(&AudioStreamState_ st, unsigned int frames) {
    if (!st.mode_out) return;
    unsigned long n = (unsigned long)frames * audio_stream_device_frame_bytes_(st, AUDIO_DIR_OUT);
    safe_memset(st.device_buf[AUDIO_DIR_OUT][0..n], 0);
}

// The bytes of one period of direction 'd''s user / device buffer.
static []unsigned char audio_user_bytes_(&AudioStreamState_ st, int d, unsigned int frames) {
    unsigned long n = (unsigned long)frames * (unsigned long)st.user_channels[d] *
                      (unsigned long)audio_format_bytes(st.user_format);
    return st.user_buf[d][0..n];
}
static []unsigned char audio_device_bytes_(&AudioStreamState_ st, int d, unsigned int frames) {
    unsigned long n = (unsigned long)frames * audio_stream_device_frame_bytes_(st, d);
    return st.device_buf[d][0..n];
}

int audio_stream_process_(&AudioStreamState_ st, unsigned int frames) {
    unsigned int n = frames;
    if (n > st.max_frames) n = st.max_frames;
    if (st.mode_in && st.convert[AUDIO_DIR_IN]) {
        audio_copy_frames(audio_user_bytes_(st, AUDIO_DIR_IN, n), st.user_format, st.user_channels[AUDIO_DIR_IN],
                          st.user_interleaved, 0,
                          audio_device_bytes_(st, AUDIO_DIR_IN, n), st.device_format[AUDIO_DIR_IN],
                          st.device_channels[AUDIO_DIR_IN], st.device_interleaved[AUDIO_DIR_IN],
                          st.first_channel[AUDIO_DIR_IN], st.user_channels[AUDIO_DIR_IN],
                          (unsigned long)n);
    }
    struct AudioIo io;
    unsigned char none_[1];
    io.output_ = none_[0..0];
    io.input_ = none_[0..0];
    if (st.mode_out) io.output_ = audio_user_bytes_(st, AUDIO_DIR_OUT, n);
    if (st.mode_in) io.input_ = audio_user_bytes_(st, AUDIO_DIR_IN, n);
    unsafe {
        long long done = atomic_load((_Atomic long long*)&st.frames_done);
        io.stream_time = (double)(done + st.frames_offset) / (double)st.sample_rate;
        io.status = atomic_exchange((_Atomic unsigned int*)&st.pending_status, 0U);
    }
    io.frames = n;
    io.output_channels = st.mode_out ? st.user_channels[AUDIO_DIR_OUT] : 0;
    io.input_channels = st.mode_in ? st.user_channels[AUDIO_DIR_IN] : 0;
    io.format = st.user_format;
    io.interleaved = st.user_interleaved;
    io.sample_rate = st.sample_rate;
    int r = AUDIO_CB_CONTINUE;
    r = st.callback(&io, st.user);
    if (st.mode_out && st.convert[AUDIO_DIR_OUT]) {
        []unsigned char dev = audio_device_bytes_(st, AUDIO_DIR_OUT, n);
        // Device channels outside the stream's range stay silent.
        if (st.device_channels[AUDIO_DIR_OUT] != st.user_channels[AUDIO_DIR_OUT]) {
            for (unsigned long i = 0UL; i < dev.len; i = i + 1UL) dev[i] = (unsigned char)0;
        }
        audio_copy_frames(dev, st.device_format[AUDIO_DIR_OUT],
                          st.device_channels[AUDIO_DIR_OUT], st.device_interleaved[AUDIO_DIR_OUT],
                          st.first_channel[AUDIO_DIR_OUT],
                          audio_user_bytes_(st, AUDIO_DIR_OUT, n), st.user_format,
                          st.user_channels[AUDIO_DIR_OUT], st.user_interleaved, 0,
                          st.user_channels[AUDIO_DIR_OUT], (unsigned long)n);
    }
    unsafe { atomic_fetch_add((_Atomic long long*)&st.frames_done, (long long)n); }
    if (r == AUDIO_CB_STOP || r == AUDIO_CB_ABORT) audio_stream_request_stop_(st, r);
    else if (r != AUDIO_CB_CONTINUE) {
        audio_stream_request_stop_(st, AUDIO_CB_ABORT);
        r = AUDIO_CB_ABORT;
    }
    return r;
}

// ── stream threads ───────────────────────────────────────────────────────

static ?&AudioStreamState_ audio_thread_tramp_(?&AudioStreamState_ arg) {
    match (arg) {
        case some(st): {
            if ((st.flags & AUDIO_FLAG_SCHEDULE_REALTIME) != 0U) audio_thread_make_realtime_(st.priority);
            match (st.thread_body) {
                case some(body): { body(st); }
                case null: {}
            }
        }
        case null: {}
    }
    return null;
}

void audio_stream_thread_join_(&AudioStreamState_ st) {
#if !defined(__wasm__)
    if (st.thread_live) {
        thread_join(st.thread);
        st.thread_live = 0;
    }
#endif
}

int audio_stream_thread_start_(&AudioStreamState_ st, fn void(&AudioStreamState_) body) {
#if defined(__wasm__)
    return audio_fail_(&st.error, AUDIO_ERR_THREAD, "audio: no threads on this target (use AUDIO_FLAG_DUMMY_MANUAL)");
#else
    audio_stream_thread_join_(st);
    st.thread_body = body;
    unsigned long long tid = 0ULL;
    int rc = 0;
    unsafe { rc = thread_create(&tid, audio_thread_tramp_, (?&AudioStreamState_)st); }
    if (rc != 0) return audio_fail_(&st.error, AUDIO_ERR_THREAD, "audio: could not start the stream thread");
    st.thread = tid;
    st.thread_live = 1;
    return AUDIO_OK;
#endif
}

// ── AudioStream ──────────────────────────────────────────────────────────

struct AudioStream AudioStream::closed() {
    struct AudioStream s;
    s.impl = null;
    return s;
}

static ?&AudioStreamState_ audio_state_of_(const &AudioStream s) {
    return s.impl;
}

static int audio_state_load_(&AudioStreamState_ st) {
    unsafe { return atomic_load((_Atomic int*)&st.state); }
}

struct AudioError AudioStream::start() {
    match (audio_state_of_(self)) {
        case some(st): {
            int s = audio_state_load_(st);
            if (s == AUDIO_STREAM_RUNNING) return AudioError::make(AUDIO_ERR_WARNING, "audio: stream already running");
            if (s != AUDIO_STREAM_STOPPED) return AudioError::make(AUDIO_ERR_INVALID_USE, "audio: stream not open");
            audio_stream_thread_join_(st);
            unsafe {
                atomic_store((_Atomic int*)&st.stop_request, 0);
                atomic_store((_Atomic unsigned int*)&st.pending_status, 0U);
                atomic_store((_Atomic int*)&st.rt_error_set, 0);
                atomic_store((_Atomic int*)&st.state, AUDIO_STREAM_RUNNING);
            }
            st.error = AudioError::none();
            int rc = AUDIO_OK;
            match (audio_stream_vt_(st)) {
                case some(vt): { unsafe { rc = vt.start(st); } }
                case null: { rc = AUDIO_ERR_INVALID_USE; }
            }
            if (rc != AUDIO_OK && rc != AUDIO_ERR_WARNING) {
                unsafe { atomic_store((_Atomic int*)&st.state, AUDIO_STREAM_STOPPED); }
                return st.error;
            }
            return AudioError::none();
        }
        case null: {}
    }
    return AudioError::make(AUDIO_ERR_INVALID_USE, "audio: stream not open");
}

static struct AudioError audio_stream_halt_(const &AudioStream s, int how) {
    match (audio_state_of_(s)) {
        case some(st): {
            int state = audio_state_load_(st);
            if (state == AUDIO_STREAM_CLOSED) return AudioError::make(AUDIO_ERR_INVALID_USE, "audio: stream not open");
            audio_stream_request_stop_(st, how);
            int rc = AUDIO_OK;
            st.error = AudioError::none();
            // A stream that already stopped itself still has its thread /
            // device to settle: the backend's stop/abort handles both.
            match (audio_stream_vt_(st)) {
                case some(vt): {
                    unsafe { rc = how == AUDIO_CB_ABORT ? vt.abort(st) : vt.stop(st); }
                }
                case null: {}
            }
            audio_stream_thread_join_(st);
            unsafe { atomic_store((_Atomic int*)&st.state, AUDIO_STREAM_STOPPED); }
            if (rc != AUDIO_OK && rc != AUDIO_ERR_WARNING) return st.error;
            if (state != AUDIO_STREAM_RUNNING) return AudioError::make(AUDIO_ERR_WARNING, "audio: stream already stopped");
            return AudioError::none();
        }
        case null: {}
    }
    return AudioError::make(AUDIO_ERR_INVALID_USE, "audio: stream not open");
}

struct AudioError AudioStream::stop() { return audio_stream_halt_(self, AUDIO_CB_STOP); }
struct AudioError AudioStream::abort() { return audio_stream_halt_(self, AUDIO_CB_ABORT); }

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

void AudioStream::drop() {
    match (audio_state_of_(self)) {
        case some(st): {
            if (audio_state_load_(st) != AUDIO_STREAM_CLOSED) {
                audio_stream_halt_(self, AUDIO_CB_ABORT);
                match (audio_stream_vt_(st)) {
                    case some(vt): { unsafe { vt.close(st); } }
                    case null: {}
                }
            }
            audio_stream_free_buffers_(st);
            unsafe { dealloc((?&heap AudioStreamState_)st); }
        }
        case null: {}
    }
    self.impl = null;
}

int AudioStream::is_open() const { return !self.impl.is_null(); }

int AudioStream::is_running() const {
    match (audio_state_of_(self)) {
        case some(st): return audio_state_load_(st) == AUDIO_STREAM_RUNNING;
        case null: {}
    }
    return 0;
}

int AudioStream::state() const {
    match (audio_state_of_(self)) {
        case some(st): return audio_state_load_(st);
        case null: {}
    }
    return AUDIO_STREAM_CLOSED;
}

double AudioStream::stream_time() const {
    match (audio_state_of_(self)) {
        case some(st): {
            long long done = 0LL;
            unsafe { done = atomic_load((_Atomic long long*)&st.frames_done); }
            if (st.sample_rate == 0U) return 0.0;
            return (double)(done + st.frames_offset) / (double)st.sample_rate;
        }
        case null: {}
    }
    return 0.0;
}

void AudioStream::set_stream_time(double seconds) {
    match (audio_state_of_(self)) {
        case some(st): {
            long long done = 0LL;
            unsafe { done = atomic_load((_Atomic long long*)&st.frames_done); }
            st.frames_offset = (long long)(seconds * (double)st.sample_rate) - done;
        }
        case null: {}
    }
}

long AudioStream::output_latency() const {
    match (audio_state_of_(self)) { case some(st): return st.mode_out ? st.latency[AUDIO_DIR_OUT] : 0L; case null: {} }
    return 0L;
}
long AudioStream::input_latency() const {
    match (audio_state_of_(self)) { case some(st): return st.mode_in ? st.latency[AUDIO_DIR_IN] : 0L; case null: {} }
    return 0L;
}
long AudioStream::latency() const { return self.output_latency() + self.input_latency(); }

unsigned int AudioStream::sample_rate() const {
    match (audio_state_of_(self)) { case some(st): return st.sample_rate; case null: {} }
    return 0U;
}
unsigned int AudioStream::buffer_frames() const {
    match (audio_state_of_(self)) { case some(st): return st.buffer_frames; case null: {} }
    return 0U;
}
int AudioStream::api() const {
    match (audio_state_of_(self)) { case some(st): return st.api; case null: {} }
    return AUDIO_API_UNSPECIFIED;
}

struct AudioError AudioStream::last_error() const {
    match (audio_state_of_(self)) {
        case some(st): {
            int set = 0;
            unsafe { set = atomic_load((_Atomic int*)&st.rt_error_set); }
            if (set == 1) return st.rt_error;
            return AudioError::none();
        }
        case null: {}
    }
    return AudioError::make(AUDIO_ERR_INVALID_USE, "audio: stream not open");
}

// The dummy backend's manual step (audio_dummy.sc).
int audio_dummy_pump_(&AudioStreamState_ st);

int AudioStream::pump() {
    match (audio_state_of_(self)) {
        case some(st): {
            if (st.api != AUDIO_API_DUMMY) return -1;
            return audio_dummy_pump_(st);
        }
        case null: {}
    }
    return -1;
}

// ── AudioIo ──────────────────────────────────────────────────────────────

static unsigned long audio_io_samples_(const &AudioIo io, int out) {
    return (unsigned long)io.frames * (unsigned long)(out ? io.output_channels : io.input_channels);
}

// 'b' viewed as n samples of an E: empty unless the stream's format is
// 'format' and b holds them.
generic<E>
static []E audio_io_view_(const &AudioIo io, []unsigned char b, int out, unsigned int format) {
    unsigned long n = audio_io_samples_(io, out);
    if (io.format != format || b.len < n * sizeof(E)) n = 0UL;
    unsafe { return ((E*)b.ptr)[0..n]; }
}

[]float AudioIo::output_f32() const { []float v = audio_io_view_(self, self.output_, 1, AUDIO_FORMAT_F32); return v; }
[]const float AudioIo::input_f32() const { []float v = audio_io_view_(self, self.input_, 0, AUDIO_FORMAT_F32); return v; }
[]double AudioIo::output_f64() const { []double v = audio_io_view_(self, self.output_, 1, AUDIO_FORMAT_F64); return v; }
[]const double AudioIo::input_f64() const { []double v = audio_io_view_(self, self.input_, 0, AUDIO_FORMAT_F64); return v; }
[]short AudioIo::output_i16() const { []short v = audio_io_view_(self, self.output_, 1, AUDIO_FORMAT_I16); return v; }
[]const short AudioIo::input_i16() const { []short v = audio_io_view_(self, self.input_, 0, AUDIO_FORMAT_I16); return v; }
[]int AudioIo::output_i32() const { []int v = audio_io_view_(self, self.output_, 1, AUDIO_FORMAT_I32); return v; }
[]const int AudioIo::input_i32() const { []int v = audio_io_view_(self, self.input_, 0, AUDIO_FORMAT_I32); return v; }
[]signed char AudioIo::output_i8() const { []signed char v = audio_io_view_(self, self.output_, 1, AUDIO_FORMAT_I8); return v; }
[]const signed char AudioIo::input_i8() const { []signed char v = audio_io_view_(self, self.input_, 0, AUDIO_FORMAT_I8); return v; }
[]S24 AudioIo::output_i24() const { []S24 v = audio_io_view_(self, self.output_, 1, AUDIO_FORMAT_I24); return v; }
[]const S24 AudioIo::input_i24() const { []S24 v = audio_io_view_(self, self.input_, 0, AUDIO_FORMAT_I24); return v; }

[]unsigned char AudioIo::output_bytes() const { return self.output_; }
[]const unsigned char AudioIo::input_bytes() const { return self.input_; }

[]float AudioIo::output_channel_f32(int channel) const {
    []float all = self.output_f32();
    if (self.interleaved || channel < 0 || channel >= self.output_channels || all.len == 0UL) return all[0..0];
    unsigned long f = (unsigned long)self.frames;
    return all[(unsigned long)channel * f..(unsigned long)(channel + 1) * f];
}
[]const float AudioIo::input_channel_f32(int channel) const {
    []const float all = self.input_f32();
    if (self.interleaved || channel < 0 || channel >= self.input_channels || all.len == 0UL) return all[0..0];
    unsigned long f = (unsigned long)self.frames;
    return all[(unsigned long)channel * f..(unsigned long)(channel + 1) * f];
}
void AudioIo::clear_output() const {
    []unsigned char b = self.output_;
    for (unsigned long i = 0UL; i < b.len; i = i + 1UL) b[i] = (unsigned char)0;
}

} // namespace std