Midi
Enumerate MIDI inputs and outputs, listen to what a controller sends, and send notes to a synth - all from C#.
@inject Bit.Butil.Midi midiMDN reference
RequestAccess has
resolved - the port list is part of the grant.
True when the runtime exposes navigator.requestMIDIAccess. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.Midi midi
var supported = await midi.IsSupported();RequestAccess prompts the user and returns the ports the grant covers. The resolved access is cached for the page, so calling it again is cheap and does not re-prompt; GetPorts re-reads the same list.
var access = await midi.RequestAccess(sysex: false);
foreach (var input in access!.Inputs) { /* input.Id, input.Name */ }
foreach (var output in access.Outputs) { /* output.Id, output.Name */ }
var current = await midi.GetPorts();Subscribes to one input, or - with no id - to every input at once, which is usually what an app wants since the user's controller is whichever one they touch.
await using var messages = await midi.SubscribeMessages(message =>
{
// message.Data[0] is the status byte: 0x90|channel is note-on
// message.TimeStamp is on the performance.now() clock
});SendNoteOn and SendNoteOff are the two messages every synth understands; Send takes raw bytes for everything else. Clear drops whatever is still queued - the way out of a note left hanging.
await midi.SendNoteOn(outputId, note: 60, velocity: 100);
await midi.SendNoteOff(outputId, note: 60);
// raw: program change to patch 5 on channel 1
await midi.Send(outputId, [0xC0, 0x05]);
await midi.Clear(outputId);Fires when a port is connected or disconnected. A port that has been unplugged stays in the list as disconnected, so a subscription survives the cable being re-seated.
await using var watch = await midi.SubscribeStateChange(port =>
{
// port?.Name, port?.State, port?.Connection
});API reference
ValueTask<bool> IsSupported()ValueTask<MidiAccessInfo?> RequestAccess(bool sysex = false, bool software = false)ValueTask<MidiAccessInfo?> GetPorts()ValueTask<bool> Send(string outputId, byte[] data, double? timestamp = null)ValueTask<bool> SendNoteOn(string outputId, byte note, byte velocity = 100, byte channel = 0) / SendNoteOff(string outputId, byte note, byte velocity = 0, byte channel = 0)ValueTask<bool> Clear(string outputId)ValueTask<ButilSubscription> SubscribeMessages(Action<MidiMessage> handler, string? inputId = null)ValueTask<ButilSubscription> SubscribeStateChange(Action<MidiPortInfo?> handler)ValueTask DisposeAsync()void InvokeMidiMessage(Guid id, MidiMessage message)void InvokeMidiStateChange(Guid id, MidiPortInfo? port)class MidiAccessInfo { bool SysexEnabled; MidiPortInfo[] Inputs; MidiPortInfo[] Outputs; }class MidiPortInfo { string Id; string? Name; string? Manufacturer; string? Version; string Type; string State; string Connection; }class MidiMessage { string PortId; byte[] Data; double TimeStamp; }