std1.0.0-beta-7 SafeC docs

Source audio/audio.h

View in the repository

// SafeC Standard Library — real-time audio I/O (in the spirit of RtAudio).
//
// One API over every platform's native audio system:
//
//   API                  platform        backend file
//   AUDIO_API_ALSA       Linux           audio_alsa_linux.sc      (libasound, loaded at run time)
//   AUDIO_API_JACK       Linux, macOS    audio_jack_posix.sc      (libjack, loaded at run time)
//   AUDIO_API_COREAUDIO  macOS           audio_coreaudio_macos.sc (Core Audio HAL: AudioObject + IOProc)
//   AUDIO_API_WASAPI     Windows         audio_wasapi_win32.sc    (shared + exclusive, event-driven)
//   AUDIO_API_WEBAUDIO   wasm32          audio_webaudio_wasm.sc   (+ the JS glue std/audio/audio_web.js)
//   AUDIO_API_DUMMY      everywhere      audio_dummy.sc           (no device: a timer thread, for tests/CI)
//
// safeguard builds the backends for the target and links what they need;
// ALSA and JACK are dlopen()ed, so a program runs (and probes zero devices)
// on a machine without them. audio_compiled_apis() lists what a build has.
//
//   struct Result<Audio, AudioError> r = std::Audio::open(AUDIO_API_UNSPECIFIED);
//   struct Audio a = r.unwrap_or(std::Audio::closed());
//   for (unsigned int i = 0U; i < a.device_count(); i = i + 1U) { ... a.device_info(i) ... }
//
//   struct AudioStreamParams out = std::AudioStreamParams::of(a.default_output_device(), 2);
//   unsigned int frames = 256U;
//   struct Result<AudioStream, AudioError> s =
//       a.open_stream((?&AudioStreamParams)&out, null, AUDIO_FORMAT_F32, 48000U, &frames,
//                     render, (?&Synth)&synth, null);
//   ... st.start(); ... st.stop(); st.close(); a.close();
//
// with the callback
//
//   int render(&AudioIo io, ?&Synth s) {
//       []float out = io.output_f32();                 // frames * channels, interleaved
//       ... fill it ...
//       return AUDIO_CB_CONTINUE;                      // or AUDIO_CB_STOP / AUDIO_CB_ABORT
//   }
//
// ── Real-time discipline ─────────────────────────────────────────────────
// The callback runs on the backend's real-time thread (an ALSA/WASAPI/dummy
// thread std starts, the JACK process thread, the Core Audio IOProc
// thread, the browser's audio rendering). Everything std does on that path
// is allocation-free and lock-free: every buffer (format conversion,
// interleaving, channel offset) is allocated by open_stream(), the stream's
// state is read and written with atomics only, and the status flags
// (AUDIO_STATUS_*) and stream time are plain atomic words. Keep your
// callback the same: no malloc/new, no locks, no blocking I/O, no printf —
// hand data to other threads through a lock-free queue (std/dsp/
// audio_buffer.h's AudioBuffer, std/sync's ring buffers). start()/stop()/
// close() are control-thread calls; they may block (joining the stream
// thread, draining the device) and must not be called from the callback —
// return AUDIO_CB_STOP or AUDIO_CB_ABORT from it instead.
//
// ── Buffers ──────────────────────────────────────────────────────────────
// The callback sees buffers in the format and layout the stream was opened
// with, whatever the device uses natively: std converts sample formats
// (i16/i24/i32/f32/f64), (de)interleaves and applies the channel offsets
// on its side. Interleaved (the default): frame-major, 'channels' samples per
// frame. With AUDIO_FLAG_NONINTERLEAVED: channel-major, 'frames' samples per
// channel. AUDIO_FORMAT_I24 is packed 3-byte little-endian samples: S24
// (std/audio/s24.h), reached through output_i24()/input_i24().
#pragma once
#include <std/audio/audio_error.h>
#include <std/audio/s24.h>
#include <std/erased.h>
#include <std/result.h>

// ── APIs ─────────────────────────────────────────────────────────────────
#define AUDIO_API_UNSPECIFIED 0   // open(): the first compiled API that works
#define AUDIO_API_ALSA        1
#define AUDIO_API_JACK        2
#define AUDIO_API_COREAUDIO   3
#define AUDIO_API_WASAPI      4
#define AUDIO_API_WEBAUDIO    5
#define AUDIO_API_DUMMY       6
#define AUDIO_API_COUNT       7

// ── Sample formats (bit flags: AudioDeviceInfo.native_formats is a set) ──
#define AUDIO_FORMAT_I8   0x01U   // signed 8-bit
#define AUDIO_FORMAT_I16  0x02U   // signed 16-bit
#define AUDIO_FORMAT_I24  0x04U   // signed 24-bit, packed 3 bytes, little-endian
#define AUDIO_FORMAT_I32  0x08U   // signed 32-bit
#define AUDIO_FORMAT_F32  0x10U   // float, nominal range [-1, 1]
#define AUDIO_FORMAT_F64  0x20U   // double, nominal range [-1, 1]

// ── Stream flags (AudioStreamOptions.flags) ──────────────────────────────
#define AUDIO_FLAG_NONINTERLEAVED   0x01U  // callback buffers channel-major
#define AUDIO_FLAG_MINIMIZE_LATENCY 0x02U  // smallest buffer the device allows
#define AUDIO_FLAG_EXCLUSIVE        0x04U  // WASAPI exclusive / Core Audio hog mode / ALSA hw: device
#define AUDIO_FLAG_SCHEDULE_REALTIME 0x08U // raise the stream thread to real-time priority
#define AUDIO_FLAG_ALSA_USE_DEFAULT 0x10U  // ALSA: open the "default" PCM for the device
#define AUDIO_FLAG_JACK_DONT_CONNECT 0x20U // JACK: register ports, don't connect them
#define AUDIO_FLAG_DUMMY_MANUAL     0x40U  // dummy: no timer thread; drive it with AudioStream::pump()

// ── Callback status (AudioIo.status) ─────────────────────────────────────
#define AUDIO_STATUS_INPUT_OVERFLOW   0x1U // input data was lost since the last callback
#define AUDIO_STATUS_OUTPUT_UNDERFLOW 0x2U // the device ran dry since the last callback

// ── Callback return values ───────────────────────────────────────────────
#define AUDIO_CB_CONTINUE 0   // keep running
#define AUDIO_CB_STOP     1   // play this buffer and what's queued, then stop
#define AUDIO_CB_ABORT    2   // stop now, dropping what's queued

// ── Stream states ────────────────────────────────────────────────────────
#define AUDIO_STREAM_CLOSED  0
#define AUDIO_STREAM_STOPPED 1
#define AUDIO_STREAM_RUNNING 2

// open_stream() 'device' meaning "the API's default device" for that direction.
#define AUDIO_DEFAULT_DEVICE 0U

#define AUDIO_DEVICE_NAME_MAX 128
#define AUDIO_MAX_SAMPLE_RATES 16
#define AUDIO_STREAM_NAME_MAX 64

namespace std {

// One audio device, as a probe saw it.
struct AudioDeviceInfo {
    unsigned int id;                       // what open_stream() takes (never 0)
    char         name[AUDIO_DEVICE_NAME_MAX];
    int          output_channels;          // max playback channels
    int          input_channels;           // max capture channels
    int          duplex_channels;          // max channels usable in both directions at once
    int          is_default_output;
    int          is_default_input;
    unsigned int native_formats;           // AUDIO_FORMAT_* the device takes without conversion
    unsigned int sample_rates[AUDIO_MAX_SAMPLE_RATES]; // supported rates, ascending
    int          sample_rate_count;
    unsigned int preferred_sample_rate;    // the device's current/natural rate

    // Whether 'rate' is in sample_rates.
    int supports_rate(unsigned int rate) const;
    // An empty record (id 0, no channels), e.g. for Result::unwrap_or().
    static struct AudioDeviceInfo none();
};

// One direction of a stream.
struct AudioStreamParams {
    unsigned int device;        // AudioDeviceInfo.id, or AUDIO_DEFAULT_DEVICE
    int          channels;      // >= 1
    int          first_channel; // offset into the device's channels (0 = first)

    static struct AudioStreamParams of(unsigned int device, int channels);
};

// Everything else a stream can be asked for. Zero-initialized (none())
// means: interleaved, the API's default buffering, normal priority.
struct AudioStreamOptions {
    unsigned int flags;         // AUDIO_FLAG_*
    unsigned int buffer_count;  // periods/buffers the device queues (0 = API default)
    int          priority;      // with AUDIO_FLAG_SCHEDULE_REALTIME: 1..99 (0 = API default)
    char         stream_name[AUDIO_STREAM_NAME_MAX]; // JACK client name, WebAudio label
    // An API-specific device to open instead of the 'device' ids: an ALSA
    // PCM name ("hw:1,0", "null", a ~/.asoundrc plugin), or for JACK the
    // client whose ports to connect to ("system"). "" = use the ids.
    char         device_name[AUDIO_DEVICE_NAME_MAX];

    static struct AudioStreamOptions none();
    static struct AudioStreamOptions with_flags(unsigned int flags);
};

// What the callback gets: the buffers for this period plus its context.
struct AudioIo {
    []unsigned char output_;    // the user-format output buffer's bytes (empty for input-only)
    []unsigned char input_;     // the user-format input buffer's bytes (empty for output-only)
    unsigned int frames;        // frames in this period
    double       stream_time;   // seconds of audio processed before this period
    unsigned int status;        // AUDIO_STATUS_* since the last callback
    int          output_channels;
    int          input_channels;
    unsigned int format;        // AUDIO_FORMAT_*
    int          interleaved;   // 0 with AUDIO_FLAG_NONINTERLEAVED
    unsigned int sample_rate;

    // The buffers, typed: empty when the stream's format is another one.
    // frames * channels samples; interleaved or channel-major as opened.
    // The input buffers are read-only.
    []float                 output_f32() const;
    []const float           input_f32() const;
    []double                output_f64() const;
    []const double          input_f64() const;
    []short                 output_i16() const;
    []const short           input_i16() const;
    []int                   output_i32() const;
    []const int             input_i32() const;
    []signed char           output_i8() const;
    []const signed char     input_i8() const;
    []S24                   output_i24() const;
    []const S24             input_i24() const;
    // Raw bytes, any format.
    []unsigned char         output_bytes() const;
    []const unsigned char   input_bytes() const;
    // One channel of a non-interleaved f32 stream ('frames' samples);
    // empty when interleaved, out of range or another format.
    []float                 output_channel_f32(int channel) const;
    []const float           input_channel_f32(int channel) const;
    // Zeroes the output buffer.
    void                    clear_output() const;
};

// The callback, erased (see std/erased.h): return AUDIO_CB_CONTINUE / _STOP / _ABORT.
typedef fn int(&AudioIo, ?&Erased) AudioCallbackRaw_;

// An open stream: a handle onto state open_stream() allocated. Copies refer
// to the same stream; close() it once.
struct AudioStream {
    ?&struct AudioStreamState_ impl;    // std-private; null when closed

    // A closed (never-opened) stream.
    static struct AudioStream closed();

    struct AudioError start();
    // Plays out what's queued, then stops (the device drains).
    struct AudioError stop();
    // Stops now, dropping what's queued.
    struct AudioError abort();
    // Stops (abort) if running and releases everything. Idempotent.
    void drop();
    void close() consume;   // = drop()

    int  is_open() const;
    // 1 while started — also 0 after the callback returned STOP/ABORT or
    // the device failed (see last_error()).
    int  is_running() const;
    int  state() const;                       // AUDIO_STREAM_*
    // Seconds of audio processed since open (frames / sample rate).
    double stream_time() const;
    void   set_stream_time(double seconds);
    // Total latency in frames (input + output) the API reports; 0 if unknown.
    long   latency() const;
    long   output_latency() const;
    long   input_latency() const;
    unsigned int sample_rate() const;         // the rate actually running
    unsigned int buffer_frames() const;       // frames per callback
    int    api() const;
    // The error that stopped the stream from its own thread (device lost,
    // xrun recovery failed), or none().
    struct AudioError last_error() const;

    // AUDIO_FLAG_DUMMY_MANUAL streams only: runs one period on the calling
    // thread — the deterministic way to drive the dummy API in tests (and
    // the only one on targets without threads). Returns the callback's
    // value, or -1 when the stream isn't a manual dummy stream or isn't running.
    int pump();
};

// A connection to one audio API: its device list and the streams it opens.
struct Audio {
    int  api_id;    // AUDIO_API_*, 0 when closed
    ?&struct AudioHost_ impl;    // std-private; null when closed

    // Opens 'api' (AUDIO_API_UNSPECIFIED: audio_default_api(), then the
    // other compiled APIs, then dummy) and probes its devices. A machine
    // with no sound hardware still opens: device_count() is 0.
    static struct Result<Audio, AudioError> open(int api);
    static struct Audio closed();

    int  is_open() const;
    int  api() const;
    // Re-enumerates the devices (after a hot-plug).
    struct AudioError rescan();
    unsigned int device_count() const;
    // The 'index'-th device (0 <= index < device_count()).
    struct Result<AudioDeviceInfo, AudioError> device_info(unsigned int index) const;
    // The device with this id.
    struct Result<AudioDeviceInfo, AudioError> device_by_id(unsigned int id) const;
    // Ids of the default devices, AUDIO_DEFAULT_DEVICE if none.
    unsigned int default_output_device() const;
    unsigned int default_input_device() const;

    // Opens a stream: output, input or both (duplex; give both params).
    // 'frames' is the requested frames per callback (0 = the API's default)
    // and receives what the device actually uses. 'opts' may be null. The
    // callback gets 'user' back as its second argument. The callback runs on
    // the audio thread while 'user' stays reachable from the caller's, so T
    // must be Send + Sync (plain data, atomics, a Mutex; no raw pointers,
    // stack references or !Send handles such as GUI windows); writes shared
    // with other threads still need atomics or a lock.
    generic<T: Send + Sync>
    struct Result<AudioStream, AudioError> open_stream(?&AudioStreamParams output, ?&AudioStreamParams input,
                                                       unsigned int format, unsigned int sample_rate,
                                                       &unsigned int frames,
                                                       fn int(&AudioIo, ?&T) callback, ?&T user,
                                                       ?&AudioStreamOptions opts);
    struct Result<AudioStream, AudioError> open_stream_raw_(?&AudioStreamParams output, ?&AudioStreamParams input,
                                                            unsigned int format, unsigned int sample_rate,
                                                            &unsigned int frames, AudioCallbackRaw_ callback,
                                                            ?&Erased user, ?&AudioStreamOptions opts);

    // Releases the device list. Streams opened from it stay valid.
    void drop();
    void close() consume;   // = drop()
};

generic<T: Send + Sync>
struct Result<AudioStream, AudioError> Audio::open_stream(?&AudioStreamParams output, ?&AudioStreamParams input,
                                                          unsigned int format, unsigned int sample_rate,
                                                          &unsigned int frames,
                                                          fn int(&AudioIo, ?&T) callback, ?&T user,
                                                          ?&AudioStreamOptions opts) {
    unsafe {
        return self.open_stream_raw_(output, input, format, sample_rate, frames,
                                     (AudioCallbackRaw_)callback, (?&Erased)user, opts);
    }
}

// ── API helpers ──────────────────────────────────────────────────────────

// The APIs compiled into this build, in preference order; returns how many
// were written to 'out' (up to out.len).
int audio_compiled_apis([]int out);
int audio_api_compiled(int api);
// The platform's usual API: Core Audio, WASAPI, WebAudio, ALSA (JACK when
// its server is running and SAFEC_AUDIO_API isn't set otherwise); the
// SAFEC_AUDIO_API environment variable ("alsa", "jack", "dummy", ...) overrides.
int audio_default_api();
// Short lowercase name ("alsa") / display name ("ALSA"); "" for unknown.
const &char audio_api_name(int api);
const &char audio_api_display_name(int api);
// AUDIO_API_* for a short name, AUDIO_API_UNSPECIFIED if none matches.
int audio_api_by_name(const &char name);

// Bytes per sample of an AUDIO_FORMAT_* (0 for an unknown/multi-bit value).
int audio_format_bytes(unsigned int format);
const &char audio_format_name(unsigned int format);

} // namespace std