MIDI API

AOT-compatible, reflection-free MIDI I/O, file handling, and hardware clock. Available as a separate package: OwnAudioSharp.Midi

🎹
MIDI I/O
Real-time input/output via WinMM, CoreMIDI, or ALSA rawmidi.
📄
MIDI File
Read, edit, and write Standard MIDI Files (SMF format 0 and 1).
🕐
MIDI Clock
Hardware-accurate 24 PPQN clock with ThreadPriority.Highest.

Namespaces

NamespaceContents
OwnAudio.Midi.IOMidiMessage, IMidiInputPort, IMidiOutputPort, MidiPortFactory
OwnAudio.Midi.FileMidiFile, MidiTrack, MidiEvent, MidiFileReader, MidiFileWriter
OwnAudio.Midi.ClockMidiClock

Port Management

Use MidiPortFactory to enumerate devices and open ports. Ports are IDisposable — always wrap in using.

C# — List available ports
using OwnAudio.Midi.IO;

IReadOnlyList<string> inputs  = MidiPortFactory.GetInputPortNames();
IReadOnlyList<string> outputs = MidiPortFactory.GetOutputPortNames();

foreach (var name in inputs)
    Console.WriteLine($"[IN]  {name}");
foreach (var name in outputs)
    Console.WriteLine($"[OUT] {name}");
C# — Open a port
// Open by index
using IMidiInputPort  input  = MidiPortFactory.OpenInput(inputs[0]);

// Open by name
using IMidiOutputPort output = MidiPortFactory.OpenOutput("IAC Driver Bus 1");
â„šī¸

Platform port names: Windows uses WinMM names (e.g. "Microsoft GS Wavetable Synth"), macOS uses CoreMIDI (e.g. "IAC Driver Bus 1"), Linux uses ALSA rawmidi paths (e.g. "/dev/midi1").

Receiving MIDI Data

Subscribe to MessageReceived before calling Start(). The callback fires on a background thread — marshal to your UI thread if needed.

C#
using IMidiInputPort input = MidiPortFactory.OpenInput("USB MIDI Interface");

input.MessageReceived += OnMidiMessage;
input.Start();

Console.WriteLine("Listening... press Enter to stop.");
Console.ReadLine();

input.Stop();
input.MessageReceived -= OnMidiMessage;

void OnMidiMessage(MidiMessage msg)
{
    if (msg.IsNoteOn)
        Console.WriteLine($"Note ON  — pitch={msg.Data1} vel={msg.Data2} ch={msg.Channel}");
    else if (msg.IsNoteOff)
        Console.WriteLine($"Note OFF — pitch={msg.Data1} ch={msg.Channel}");
    else if (msg.IsControlChange)
        Console.WriteLine($"CC #{msg.Data1} = {msg.Data2}  ch={msg.Channel}");
    else if (msg.IsPitchBend)
    {
        int bend = (msg.Data2 << 7) | msg.Data1; // 0..16383, centre = 8192
        Console.WriteLine($"Pitch Bend = {bend}  ch={msg.Channel}");
    }
}

MidiMessage Properties

PropertyTypeDescription
StatusbyteRaw status byte.
Data1byteFirst data byte (note number, CC number, â€Ļ).
Data2byteSecond data byte (velocity, CC value, â€Ļ).
TimestamplongNanoseconds since port open (platform-dependent precision).
TypeMidiMessageTypeEnum: NoteOn, NoteOff, ControlChange, â€Ļ
ChannelintMIDI channel 0–15.
IsNoteOnbooltrue if NoteOn with velocity > 0.
IsNoteOffbooltrue if NoteOff, or NoteOn with velocity = 0.
IsControlChangeboolCC message.
IsPitchBendboolPitch bend message.

Sending MIDI Data

C#
using IMidiOutputPort output = MidiPortFactory.OpenOutput("USB MIDI Interface");

// Note On / Note Off  (status = 0x90 | channel)
output.Send(new MidiMessage(0x90, 60, 100)); // Middle C, velocity 100
await Task.Delay(500);
output.Send(new MidiMessage(0x80, 60, 0));   // Note Off

// Control Change  (status = 0xB0 | channel)
output.Send(new MidiMessage(0xB0, 7, 100));  // Volume (CC #7) = 100

// Program Change  (status = 0xC0 | channel)
output.Send(new MidiMessage(0xC0, 0, 0));    // Grand Piano

// Pitch Bend — 14-bit split across LSB/MSB
int bend = 10000; // 0..16383, centre = 8192
output.Send(new MidiMessage(0xE0, (byte)(bend & 0x7F), (byte)(bend >> 7)));

// SysEx
ReadOnlySpan<byte> sysex = [0xF0, 0x41, 0x10, 0x42, 0x12, 0xF7];
output.SendSysEx(sysex);

Common Status Bytes

MessageStatus byteData1Data2
Note Off0x80 | chnote (0–127)velocity
Note On0x90 | chnote (0–127)velocity
Aftertouch0xA0 | chnotepressure
Control Change0xB0 | chCC numbervalue
Program Change0xC0 | chprogram—
Channel Pressure0xD0 | chpressure—
Pitch Bend0xE0 | chLSB (7-bit)MSB (7-bit)

MIDI File

MidiFileReader parses Standard MIDI Files (SMF format 0 & 1). MidiFile, MidiTrack, and MidiEvent are immutable — edit by rebuilding the event list. Namespace: OwnAudio.Midi.File

C# — Reading a MIDI file
using OwnAudio.Midi.File;

MidiFile file = MidiFileReader.Read("song.mid");

Console.WriteLine($"Format: {file.Format}  Ticks/beat: {file.TicksPerBeat}");
Console.WriteLine($"Tracks: {file.Tracks.Count}");

foreach (MidiTrack track in file.Tracks)
{
    long absoluteTick = 0;

    foreach (MidiEvent evt in track.Events)
    {
        absoluteTick += evt.DeltaTime;

        if (evt.Type == MidiEventType.Midi)
        {
            byte msgType = (byte)(evt.Status & 0xF0);
            int  channel = evt.Status & 0x0F;

            if (msgType == 0x90 && evt.Data2 > 0)
                Console.WriteLine($"t={absoluteTick,8}  NoteOn  ch={channel} note={evt.Data1} vel={evt.Data2}");
        }
        else if (evt.Type == MidiEventType.Meta && evt.IsTempoChange)
        {
            double bpm = 60_000_000.0 / evt.GetTempoMicroseconds();
            Console.WriteLine($"t={absoluteTick,8}  Tempo {bpm:F1} BPM");
        }
    }
}

// Convert ticks → seconds  (tempo-dependent)
int tempoUs = 500_000; // default 120 BPM
double TicksToSeconds(long ticks) =>
    ticks * tempoUs / (1_000_000.0 * file.TicksPerBeat);
C# — Editing (transpose all notes up 2 semitones)
MidiFile original = MidiFileReader.Read("song.mid");

var editedEvents = new List<MidiEvent>();
foreach (MidiEvent evt in original.Tracks[0].Events)
{
    if (evt.Type == MidiEventType.Midi)
    {
        byte msgType = (byte)(evt.Status & 0xF0);
        if (msgType == 0x90 || msgType == 0x80)
        {
            byte newNote = (byte)Math.Clamp(evt.Data1 + 2, 0, 127);
            editedEvents.Add(new MidiEvent(evt.DeltaTime, evt.Status, newNote, evt.Data2));
            continue;
        }
    }
    editedEvents.Add(evt);
}

var editedFile = new MidiFile(
    original.Format,
    original.TicksPerBeat,
    [new MidiTrack(editedEvents)]);

MidiFileWriter.Write(editedFile, "song_transposed.mid");
C# — Writing from scratch (C major scale)
const ushort TicksPerBeat = 480;
const int Bpm = 120;
int tempoUs = 60_000_000 / Bpm;

var events = new List<MidiEvent>();

byte[] tempoBytes = [(byte)(tempoUs >> 16), (byte)(tempoUs >> 8), (byte)tempoUs];
events.Add(new MidiEvent(0, 0x51, tempoBytes)); // Tempo meta event

events.Add(new MidiEvent(0, 0xC0, 0, 0)); // Program Change — piano

int[] scale = [60, 62, 64, 65, 67, 69, 71, 72]; // C D E F G A B C
long cursor = 0;
long T(double beats) => (long)(beats * TicksPerBeat);

for (int i = 0; i < scale.Length; i++)
{
    int deltaOn  = (int)(T(i)       - cursor); cursor = T(i);
    int deltaOff = (int)(T(i + 0.9) - cursor); cursor = T(i + 0.9);

    events.Add(new MidiEvent(deltaOn,  0x90, (byte)scale[i], 80));
    events.Add(new MidiEvent(deltaOff, 0x80, (byte)scale[i],  0));
}

// End of Track is appended automatically by MidiFileWriter
var midiFile = new MidiFile(0, TicksPerBeat, [new MidiTrack(events)]);
MidiFileWriter.Write(midiFile, "c_major_scale.mid");

MidiEvent Properties

PropertyTypeDescription
DeltaTimeintTicks since previous event.
TypeMidiEventTypeMidi, Meta, or SysEx.
StatusbyteStatus byte (Midi events) or meta type (Meta events).
Data1byteFirst data byte.
Data2byteSecond data byte.
MetaDatabyte[]?Raw payload for Meta and SysEx events.
IsTempoChangebooltrue for Meta type 0x51 (Set Tempo).
IsEndOfTrackbooltrue for Meta type 0x2F (End of Track).
GetTempoMicroseconds()intMicroseconds per beat (only valid on tempo-change events).

MIDI Clock

Sends MIDI Timing Clock pulses (0xF8) at 24 PPQN. The clock thread runs at ThreadPriority.Highest using a spin-wait loop for microsecond accuracy. Namespace: OwnAudio.Midi.Clock

C# — Internal clock (sequencer use)
using OwnAudio.Midi.Clock;

using var clock = new MidiClock(bpm: 120.0);
clock.Start();

await Task.Delay(4000);

clock.Bpm = 140.0; // change tempo while running

await Task.Delay(4000);
clock.Stop();
C# — Clock with hardware output port
using OwnAudio.Midi.IO;
using OwnAudio.Midi.Clock;

using IMidiOutputPort output = MidiPortFactory.OpenOutput("USB MIDI Interface");
using var clock = new MidiClock(bpm: 120.0, outputPort: output);

clock.Start();    // sends 0xFA (MIDI Start) then 0xF8 clock pulses
// ...
clock.Stop();     // sends 0xFC (MIDI Stop)
clock.Continue(); // sends 0xFB (MIDI Continue) and resumes

Automatic Clock Messages

EventMIDI MessageByte
Start()MIDI Start0xFA
Stop()MIDI Stop0xFC
Continue()MIDI Continue0xFB
Every pulse (24/beat)MIDI Timing Clock0xF8

Full Example — MIDI Keyboard Recorder

Records incoming MIDI events with timestamps and saves to a Standard MIDI File.

C#
using OwnAudio.Midi.IO;
using OwnAudio.Midi.File;

var recordedEvents = new List<(long absoluteTick, MidiEvent evt)>();
var startTime = DateTimeOffset.UtcNow;
const ushort TicksPerBeat = 480;
const int Bpm = 120;
int tempoUs = 60_000_000 / Bpm;
double ticksPerMs = TicksPerBeat * Bpm / 60_000.0;

using IMidiInputPort input = MidiPortFactory.OpenInput(
    MidiPortFactory.GetInputPortNames()[0]);

input.MessageReceived += msg =>
{
    long ms   = (long)(DateTimeOffset.UtcNow - startTime).TotalMilliseconds;
    long tick = (long)(ms * ticksPerMs);
    recordedEvents.Add((tick, new MidiEvent(0, msg.Status, msg.Data1, msg.Data2)));
};

input.Start();
Console.WriteLine("Recording... press Enter to stop.");
Console.ReadLine();
input.Stop();

// Convert absolute ticks to delta times
recordedEvents.Sort((a, b) => a.absoluteTick.CompareTo(b.absoluteTick));
var midiEvents = new List<MidiEvent>();

byte[] tempoBytes = [(byte)(tempoUs >> 16), (byte)(tempoUs >> 8), (byte)tempoUs];
midiEvents.Add(new MidiEvent(0, 0x51, tempoBytes));

long prev = 0;
foreach (var (tick, evt) in recordedEvents)
{
    int delta = (int)(tick - prev);
    prev = tick;
    midiEvents.Add(new MidiEvent(delta, evt.Status, evt.Data1, evt.Data2));
}

var midiFile = new MidiFile(0, TicksPerBeat, [new MidiTrack(midiEvents)]);
MidiFileWriter.Write(midiFile, "recording.mid");
Console.WriteLine("Saved: recording.mid");

Platform Support

PlatformI/O BackendFile R/WClock
WindowsWinMM (winmm.dll)✅✅
macOSCoreMIDI framework✅✅
LinuxALSA rawmidi (libasound)✅✅
â„šī¸

All platform code is selected at compile time via #if WINDOWS / MACOS / LINUX — no runtime reflection. The library is fully compatible with .NET Native AOT (IsAotCompatible=true, IsTrimmable=true). All P/Invoke uses [LibraryImport] (source-generated, AOT-safe).