std1.0.0-beta-7 SafeC docs

Source audio/midi.h

View in the repository

// SafeC Standard Library — real-time MIDI I/O (in the spirit of RtMidi).
//
//   API                 platform        backend file
//   MIDI_API_ALSA       Linux           midi_alsa_linux.sc     (ALSA sequencer, libasound loaded at run time)
//   MIDI_API_JACK       Linux, macOS    midi_jack_posix.sc     (JACK MIDI, libjack loaded at run time)
//   MIDI_API_COREMIDI   macOS           midi_coremidi_macos.sc
//   MIDI_API_WINMM      Windows         midi_winmm_win32.sc    (the Windows multimedia MIDI API)
//   MIDI_API_WEBMIDI    wasm32          midi_webmidi_wasm.sc   (+ std/audio/audio_web.js)
//   MIDI_API_DUMMY      everywhere      midi_dummy.sc          (an in-process loopback, for tests)
//
// A MidiIn or MidiOut is one client of an API. It lists the system's ports,
// connects to one (open_port) or creates its own that other programs
// connect to (open_virtual_port: ALSA, JACK, Core MIDI and dummy — WinMM
// and Web MIDI have no virtual ports and return AUDIO_ERR_UNSUPPORTED).
//
//   struct Result<MidiIn, AudioError> r = std::MidiIn::open(MIDI_API_UNSPECIFIED, "monitor", 0UL);
//   struct MidiIn in = r.unwrap_or(std::MidiIn::closed());
//   in.open_port(0U, "in");
//   in.set_callback(on_midi, (?&State)&state);      // or poll: in.get_message(...)
//
//   struct MidiOut out = std::MidiOut::open(MIDI_API_UNSPECIFIED, "synth").unwrap_or(std::MidiOut::closed());
//   out.open_port(0U, "out");
//   out.send(std::MidiMsg::note_on(0, 60, 100));
//
// Input arrives on the backend's thread (the ALSA sequencer thread std
// starts, JACK's process thread, Core MIDI's and WinMM's callback threads,
// the browser's event loop). With a callback set, it is called there with
// each complete message — sysex reassembled, running status expanded — and
// must follow the same real-time rules as an audio callback (see audio.h):
// no allocation, no locks, no blocking. Without one, messages go into a
// lock-free single-producer/single-consumer queue (sized at open, default
// MIDI_DEFAULT_QUEUE_BYTES) that get_message() drains from any one thread;
// a message that doesn't fit is dropped and counted (dropped()). Sysex,
// MIDI clock/time code and active sensing are filtered out by default
// (ignore_types(1, 1, 1)), as RtMidi does.
//
// Timestamps are seconds since the port was opened (monotonic); each event
// also carries the time since the previous delivered message.
#pragma once
#include <std/audio/audio_error.h>
#include <std/erased.h>
#include <std/result.h>

#define MIDI_API_UNSPECIFIED 0
#define MIDI_API_ALSA        1
#define MIDI_API_JACK        2
#define MIDI_API_COREMIDI    3
#define MIDI_API_WINMM       4
#define MIDI_API_WEBMIDI     5
#define MIDI_API_DUMMY       6
#define MIDI_API_COUNT       7

#define MIDI_PORT_NAME_MAX       128
#define MIDI_CLIENT_NAME_MAX     64
#define MIDI_DEFAULT_QUEUE_BYTES 16384UL
#define MIDI_MAX_SYSEX           65536UL   // longest sysex std reassembles

// ── Status bytes ─────────────────────────────────────────────────────────
#define MIDI_NOTE_OFF          0x80
#define MIDI_NOTE_ON           0x90
#define MIDI_POLY_PRESSURE     0xA0
#define MIDI_CONTROL_CHANGE    0xB0
#define MIDI_PROGRAM_CHANGE    0xC0
#define MIDI_CHANNEL_PRESSURE  0xD0
#define MIDI_PITCH_BEND        0xE0
#define MIDI_SYSEX             0xF0
#define MIDI_TIME_CODE         0xF1
#define MIDI_SONG_POSITION     0xF2
#define MIDI_SONG_SELECT       0xF3
#define MIDI_TUNE_REQUEST      0xF6
#define MIDI_SYSEX_END         0xF7
#define MIDI_CLOCK             0xF8
#define MIDI_START             0xFA
#define MIDI_CONTINUE          0xFB
#define MIDI_STOP              0xFC
#define MIDI_ACTIVE_SENSING    0xFE
#define MIDI_SYSTEM_RESET      0xFF

// MidiParser::feed() results.
#define MIDI_PARSE_NONE    0   // byte consumed, nothing complete yet
#define MIDI_PARSE_MESSAGE 1   // a channel / system common / real-time message: message()
#define MIDI_PARSE_SYSEX   2   // a complete sysex (F0 .. F7) in the sysex buffer
#define MIDI_PARSE_ERROR   3   // a stray data byte or a sysex too long for the buffer (dropped)

namespace std {

// A MIDI message of up to 3 bytes: everything but sysex.
struct MidiMsg {
    unsigned char data[3];
    int           len;

    static struct MidiMsg note_on(int channel, int note, int velocity);
    static struct MidiMsg note_off(int channel, int note, int velocity);
    static struct MidiMsg poly_pressure(int channel, int note, int pressure);
    static struct MidiMsg control_change(int channel, int controller, int value);
    static struct MidiMsg program_change(int channel, int program);
    static struct MidiMsg channel_pressure(int channel, int pressure);
    // value: -8192 .. 8191 (0 = centre)
    static struct MidiMsg pitch_bend(int channel, int value);
    static struct MidiMsg song_position(int beats);     // 0 .. 16383 sixteenth notes
    static struct MidiMsg song_select(int song);
    // A one-byte system message: MIDI_CLOCK, MIDI_START, MIDI_STOP, ...
    static struct MidiMsg system(int status);
    // 'bytes' (1..3 of them) as a message (len 0 if it's empty or longer).
    static struct MidiMsg of([]const unsigned char bytes);
    // The message's bytes (data[0..len]).
    []const unsigned char bytes() const;

    int status() const;       // the status byte (0 for an empty message)
    int type() const;         // status & 0xF0 for channel messages, else the status
    int channel() const;      // 0..15 for channel messages, -1 otherwise
    int data1() const;        // first data byte (note, controller, program), -1 if none
    int data2() const;        // second data byte (velocity, value), -1 if none
    int is_note_on() const;   // note on with velocity > 0
    int is_note_off() const;  // note off, or note on with velocity 0
    int pitch_bend_value() const;   // -8192 .. 8191 for a pitch bend, 0 otherwise
};

// Expected length of a message starting with 'status': 1..3, 0 for sysex
// (variable), -1 for a data byte (< 0x80) or an undefined status.
int midi_message_length(int status);
// Whether 'status' is a real-time byte (0xF8..0xFF), which may appear
// between the bytes of any other message.
int midi_is_realtime(int status);
// F0 <payload> F7 into 'out'; the length written, or -1 if it doesn't fit
// or the payload has a byte >= 0x80.
long midi_build_sysex([]unsigned char out, []const unsigned char payload);
// 14-bit value from LSB/MSB data bytes, and back.
int  midi_14bit(int lsb, int msb);

// A byte-stream parser: running status, real-time bytes interleaved with
// other messages, sysex reassembly into a caller-owned buffer. Allocation-
// free; safe on a real-time thread.
struct MidiParser {
    unsigned char running;      // current running status (0 = none)
    unsigned char msg[3];
    int           have;         // bytes in msg
    int           need;         // bytes msg needs
    struct MidiMsg last;        // the last complete short message
    []unsigned char sysex;      // sysex buffer (empty: sysex dropped)
    unsigned long sysex_len;
    int           in_sysex;
    int           sysex_overflow;

    // 'sysex_buf' receives reassembled sysex messages (F0 .. F7); pass an
    // empty slice to drop them.
    static struct MidiParser new([]unsigned char sysex_buf);
    // One byte in; MIDI_PARSE_*.
    int  feed(int byte);
    struct MidiMsg message() const;
    unsigned long sysex_length() const;
    // The last complete sysex (after MIDI_PARSE_SYSEX): sysex[0..sysex_len].
    []const unsigned char sysex_message() const;
    void reset();
};

// The reverse: writes messages for a byte stream, dropping the status byte
// when running status allows (channel messages with the previous status).
struct MidiEncoder {
    unsigned char running;
    int           use_running_status;

    static struct MidiEncoder new(int use_running_status);
    // Bytes written to 'out', or -1 if it is too small.
    long encode([]const unsigned char msg, []unsigned char out);
    void reset();
};

// One input message, as a callback sees it. 'bytes' is valid only during
// the callback.
struct MidiEvent {
    double                     timestamp;   // seconds since the port was opened
    double                     delta;       // seconds since the previous message
    []const unsigned char      bytes;       // the complete message

    int            status() const;
    // The message as a MidiMsg (len 0 for sysex).
    struct MidiMsg short_msg() const;
};

struct MidiPortInfo {
    unsigned int index;
    char         name[MIDI_PORT_NAME_MAX];
};

// The input callback, erased (see std/erased.h).
typedef fn void(&MidiEvent, ?&Erased) MidiCallbackRaw_;

struct MidiIn {
    int    api_id;
    ?&struct MidiPortState_ impl;     // std-private; null when closed

    // A client of 'api' (MIDI_API_UNSPECIFIED: midi_default_api()) named
    // 'client_name' (what other programs see); 'queue_bytes' sizes the
    // polling queue (0 = MIDI_DEFAULT_QUEUE_BYTES).
    static struct Result<MidiIn, AudioError> open(int api, const &char client_name, unsigned long queue_bytes);
    static struct MidiIn closed();

    int  is_open() const;
    int  api() const;
    unsigned int port_count();
    struct Result<MidiPortInfo, AudioError> port_info(unsigned int index);
    // Connects to the index-th port; 'port_name' names our end.
    struct AudioError open_port(unsigned int index, const &char port_name);
    // Creates a port other programs can connect to.
    struct AudioError open_virtual_port(const &char port_name);
    void close_port();
    int  is_port_open() const;

    // The callback runs on the API's receiving thread, so T must be Send +
    // Sync (as for Audio::open_stream).
    generic<T: Send + Sync> void set_callback(fn void(&MidiEvent, ?&T) callback, ?&T user);
    void set_callback_raw_(MidiCallbackRaw_ callback, ?&Erased user);
    // Back to the polling queue.
    void cancel_callback();
    // Which message kinds to drop (non-zero = drop): sysex, clock/time code, active sensing.
    void ignore_types(int sysex, int timing, int active_sensing);

    // Polling: copies the next queued message into 'buf' and returns its
    // length (its timestamp into 'timestamp'); 0 when the queue is empty;
    // -(length) when 'buf' is too small (the message stays queued).
    long get_message([]unsigned char buf, &double timestamp);
    // Messages lost to a full queue or an oversized sysex.
    unsigned long dropped() const;
    void drop();

    void close() consume;   // = drop()
};

struct MidiOut {
    int    api_id;
    ?&struct MidiPortState_ impl;     // std-private; null when closed

    static struct Result<MidiOut, AudioError> open(int api, const &char client_name);
    static struct MidiOut closed();

    int  is_open() const;
    int  api() const;
    unsigned int port_count();
    struct Result<MidiPortInfo, AudioError> port_info(unsigned int index);
    struct AudioError open_port(unsigned int index, const &char port_name);
    struct AudioError open_virtual_port(const &char port_name);
    void close_port();
    int  is_port_open() const;

    // Sends one complete message (a short message or a whole sysex).
    struct AudioError send_message([]const unsigned char msg);
    struct AudioError send(struct MidiMsg msg);
    void drop();

    void close() consume;   // = drop()
};

generic<T: Send + Sync>
void MidiIn::set_callback(fn void(&MidiEvent, ?&T) callback, ?&T user) {
    unsafe { self.set_callback_raw_((MidiCallbackRaw_)callback, (?&Erased)user); }
}

// The APIs compiled into this build, in preference order (up to out.len).
int midi_compiled_apis([]int out);
int midi_api_compiled(int api);
// Core MIDI, WinMM, Web MIDI, ALSA (JACK when its server is running); the
// SAFEC_MIDI_API environment variable ("alsa", "jack", "dummy", ...) overrides.
int midi_default_api();
const &char midi_api_name(int api);
const &char midi_api_display_name(int api);
int midi_api_by_name(const &char name);

} // namespace std