MIDI#

Standard MIDI file playback (MidiFile), output port (MidiOut), and input port (MidiIn).

Upstream: YSE::midiOut / midiIn.

MidiFile#

class MidiFile implements Finalizable

Standard MIDI-file playback.

Load a .mid file with [load], then control playback with [play] / [pause] / [stop].

Constructors#

factory MidiFile(String filename)

Load [filename] and prepare for playback. Throws [YseException] on failure.

Methods#

void play()

Start or resume playback.

void pause()

Pause playback. Resume with [play].

void stop()

Stop playback and rewind to the start.

void connectSynth(Synth synth)

Route this file’s playback into an internal [synth] (upstream #372).

While the file plays, every note / controller / pitch-bend event it contains is delivered to [synth] block-accurately on the audio thread. May be called for several synths (up to a small fixed cap) to drive them together; re-connecting an already-connected synth is a no-op. [synth] must outlive the connection — [disconnectSynth] it (or dispose this file) before disposing the synth. The file holds a Dart reference to [synth] until then, so garbage collection never frees it under the connection.

void disconnectSynth(Synth synth)

Stop routing this file’s playback into [synth]. Safe to call for a synth that was never connected.

void dispose()

Destroy the underlying native file and detach the finalizer.

MidiOut#

class MidiOut implements Finalizable

MIDI output port (Windows / Linux only; backed by RtMidi upstream).

Enumerate device counts with [System.midiOutDeviceCount] and names with [System.midiOutDeviceName], then open one with [MidiOut.open].

Channels here are wire-level, 0-based (0..15) — the low nibble of the status byte. The musical layer ([Synth], [ClipEvent], Note) counts 1..16 instead, so MIDI channel 1 is 0 here and 1 there. A channel outside 0..15 is clamped into that range by the engine (libYSE 3.0.1) rather than spilling into the status nibble, so -1 sends on channel 0 and 16 on channel 15.

To send a message the typed helpers don’t cover — a SysEx dump, or any other complete message — use [raw].

Constructors#

factory MidiOut.open(int port)

Open the MIDI device at [port].

When [port] does not exist or the MIDI backend cannot start, the engine reports the RtMidi error through [Log] and the port stays unopened: every send on it is then a silent no-op. Check [port] against [System.midiOutDeviceCount] first.

Properties#

set localControl(bool on)

Toggle local-keyboard control on the receiving instrument.

set omni(bool on)

Toggle Omni mode on the receiving instrument.

set poly(bool on)

Toggle Poly (true) or Mono (false) mode on the receiving instrument.

Pointer<YseMidiOut> get handle

Internal: the raw native handle, for wiring this port as a [ClipTransport] sink. Not part of the public surface.

Methods#

void noteOn({required int channel, required int pitch, required int velocity})

Send Note-On. [channel] is 0..15, [pitch] is 0..127.

void noteOff({required int channel, required int pitch, int velocity = 0})

Send Note-Off.

void polyPressure({required int channel, required int pitch, required int value})

Send polyphonic key-pressure (aftertouch).

void channelPressure({required int channel, required int value})

Send channel-pressure.

void programChange({required int channel, required int value})

Send program-change.

void controlChange({required int channel, required int controller, required int value})

Send control-change.

void allNotesOff({int? channel})

Send All-Notes-Off on a specific [channel] (0..15), or on every channel when null.

void reset({int? channel})

Send Reset-All-Controllers on a specific [channel] (or all if null).

void raw3(int a, int b, int c)

Send three raw MIDI bytes.

Always sends exactly three bytes, so it only suits three-byte messages (note on/off, poly pressure, control change, pitch bend). For anything else — a two-byte program change or channel pressure, a one-byte realtime message, SysEx — use [raw], which sends the message as given.

void raw(List<int> bytes)

Send one complete MIDI message, byte for byte — no padding and no truncation (libYSE #748 / #903).

This is the way to send two-byte messages (program change, channel pressure), single-byte system realtime messages, and System Exclusive dumps of any length:

out.raw([0xC0, 5]);                               // program change
out.raw([0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]);    // GM System On

[bytes] is copied into native memory for the call, so it may be reused or modified straight afterwards. An empty list is a no-op.

The message is checked before anything reaches the engine, and an [ArgumentError] is thrown when:

  • a value is outside 0..255;

  • the first byte is not a status byte (>= 0x80) — running status is not supported, every message carries its own status;

  • a data byte has its high bit set (>= 0x80);

  • the length doesn’t match the status: 3 bytes for note off/on, poly pressure, control change, pitch bend and song position; 2 for program change, channel pressure, MTC quarter frame and song select; 1 for tune request and system realtime (0xF8..0xFF);

  • a SysEx message (0xF0) doesn’t end with 0xF7;

  • the status is 0xF4 / 0xF5 (undefined) or a stray 0xF7.

Unlike the typed helpers, the channel nibble of a raw status byte is sent as given — there is nothing to clamp.

On a port that isn’t open (see [MidiOut.open]) or after [dispose] this is a silent no-op, like every other send. A send RtMidi refuses is reported through [Log] and dropped; it never throws across the FFI boundary.

void dispose()

Destroy the underlying native port and detach the finalizer.

MidiIn#

class MidiIn implements Finalizable

MIDI input port (Windows / Linux only; backed by RtMidi upstream).

Enumerate device counts with [System.midiInDeviceCount] and names with [System.midiInDeviceName], then open one with [MidiIn.open].

Two modes of consumption — subscribe to either or both:
  • [rawMessages] — full message bytes as a [Uint8List].

  • [parsedMessages] — pre-decoded channel-voice scalars.

Channel numbering has two bases. The parsed stream reports the wire-level, 0-based channel (0..15, see [MidiInParsedMessage.channel]), while [connectSynth]’s channelFilter uses the musical layer’s 1-based numbering (1..16, 0 = every channel) shared with [Synth] and [ClipEvent].

Native callbacks fire on RtMidi’s internal input thread. NativeCallable.listener posts each event back to the isolate that created this MidiIn, so Dart code only ever sees messages serialised in the isolate’s event queue.

Constructors#

factory MidiIn.open(int port)

Open the MIDI input device at [port].

Properties#

bool get isOpen

Whether the underlying RtMidi port is currently open.

Stream<MidiInRawMessage> get rawMessages

Broadcast stream of raw MIDI messages. Lazily installs the native callback on first subscription and uninstalls when the last subscriber cancels.

Messages still queued when the last subscriber cancels (or the port is [dispose]d) are not delivered, but their native buffers are still released.

Stream<MidiInParsedMessage> get parsedMessages

Broadcast stream of pre-decoded MIDI messages. Lazily installs the native callback on first subscription and uninstalls when the last subscriber cancels.

Methods#

void close()

Close the port. The instance remains valid but emits no further messages until re-created.

void connectSynth(Synth synth, {int channelFilter = 0})

Route incoming device MIDI into an internal [synth] (upstream #371).

Every channel-voice message received on the open port is mapped to the synth’s normalized note API and pushed onto its inbox on RtMidi’s input thread. [channelFilter] is a 1-based 1..16 MIDI channel to accept, or 0 (the default) for every channel — note the parsed stream reports the same channel 0-based, so channel 1 there is filter 1 here. May be called for several synths (up to a small fixed cap); re-connecting an already-connected synth just updates its channel filter. [synth] must outlive the connection — [disconnectSynth] it (or close/dispose this port) before disposing the synth. The port holds a Dart reference to [synth] until [disconnectSynth] or [dispose], so garbage collection never frees it under the connection.

void disconnectSynth(Synth synth)

Stop routing incoming device MIDI into [synth]. Safe to call for a synth that was never connected.

void dispose()

Destroy the underlying native port and detach the finalizer.

Also clears both callbacks and closes [rawMessages] and [parsedMessages] (listeners get onDone).

Message types#

MidiInRawMessage#

class MidiInRawMessage

A raw MIDI message delivered through [MidiIn.rawMessages].

Bytes are copied out of the engine-owned buffer before the underlying allocation is released, so [bytes] is safe to retain.

Constructors#

MidiInRawMessage(this.timestamp, this.bytes)

Constructor — invoked internally by [MidiIn.rawMessages].

Properties#

final double timestamp

Timestamp from RtMidi, in seconds (monotonically increasing within a single port session).

final Uint8List bytes

Full MIDI message bytes, exactly as RtMidi delivered them.

MidiInParsedMessage#

class MidiInParsedMessage

A pre-decoded channel-voice MIDI message delivered through [MidiIn.parsedMessages].

[status] is the high nibble of the first byte (0x80..`0xF0`), [channel] is the low nibble (0..`15`). [data1] and [data2] are 0 for messages shorter than 3 bytes.

Constructors#

MidiInParsedMessage(this.timestamp, this.status, this.channel, this.data1, this.data2)

Constructor — invoked internally by [MidiIn.parsedMessages].

Properties#

final double timestamp

Timestamp from RtMidi, in seconds.

final int status

Status nibble (0x80..`0xF0`) — Note-On, CC, etc.

final int channel

Wire-level MIDI channel, 0-based (0..`15`). Add one to address the same channel on a [Synth] or [ClipEvent], which count 1..16.

final int data1

First data byte (e.g. pitch for Note-On, controller for CC).

final int data2

Second data byte (e.g. velocity for Note-On, value for CC). 0 for 1- or 2-byte messages.

Channel numbering#

MIDI channels are counted from two different bases, depending on the layer:

Layer

Range

Used by

Wire level (status-byte low nibble)

0..15

MidiOut sends, MidiInParsedMessage channel

Musical layer

1..16; 0 = omni on a synth call

Synth, ClipEvent, Note / PNote, the MidiIn.connectSynth channelFilter

So MIDI channel 1 is 0 on a MidiOut and 1 on a Synth. On a synth call 0 means every channel, never channel 1. A ClipTransport routed to a MidiOut converts for you (channel - 1 on the wire); add one yourself when feeding a parsed MidiIn message into a synth by hand.