aud_midi_web 0.1.1
aud_midi_web: ^0.1.1 copied to clipboard
Web MIDI backend for aud_midi, built on package:web
aud_midi_web #
The browser backend of aud_midi, built on the Web MIDI API through package:web.
Part of the aud_midi family, see aud_midi.
Goals #
- Web MIDI inputs, outputs and hotplug
- Sysex and scheduled send
- Translation to MIDI 2.0 on top of byte ports
State #
WebMidiBackend (backend name webmidi) implements MidiBackend of
aud_midi_core:
startcallsnavigator.requestMIDIAccess({sysex, software}). No Web MIDI or no secure context →MidiUnsupported; a refused permission →MidiPermissionDenied(sysExwhen requested, elsemidi); any other failure →MidiNativeErrorplus anativeErrordiagnostic- Every Web MIDI port becomes a MIDI 1.0 byte port on a synthetic one-port
device, id
webmidi:input:<MIDIPort.id>orwebmidi:output:<MIDIPort.id>, transportunknown; MIDI 2.0 comes from the engine's translation - Inputs deliver
MidiBytesPackets with the browser's receive time on the package clock (MidiClockMapperonperformance.now(), measured again every 10 s and on hotplug) - Outputs schedule in the browser:
send(data, timestamp)with the due time. Bytes the browser would refuse (running status, split or unpermitted SysEx, undefined status bytes, incomplete messages) and exceptions of the browser become diagnostics cancelPendingusesMIDIOutput.clear()where the browser has it; Chrome 153 has not, socapabilities.cancelPendingis false there- Hotplug through
onstatechange→MidiPortAdded,MidiPortChanged,MidiPortRemoved; an open port that disappears is closed - No virtual ports, Bluetooth scan or network sessions: Web MIDI has none
Verified:
- The logic on the Dart VM against a fake of the browser interface, 100 %
coverage per file:
dart test - In headless Chrome 153 with dart2js and dart2wasm:
dart test -P browserloads the adapter, requests access with and without SysEx and starts and stops the backend on the real API. Headless Chrome denies permission prompts;dart_test.yamlpasses--disable-features=BlockMidiByDefault, which grants MIDI without SysEx. SysEx stays denied there - Through a CoreMIDI loopback (
tool/macos_midi_loopback.swift): round trip with receive stamps within 1 ms of the send call, sends scheduled 100 ms ahead arrive at their due time (±0.1 ms), one packet arrives as one event per message, refused bytes become diagnostics. Hotplug by hand with the tool's delay and lifetime
Usage notes #
- Web MIDI needs a secure context: https or localhost. Elsewhere
navigator.requestMIDIAccessis missing andstartthrowsMidiUnsupported startmakes the browser ask for permission; current Chrome versions ask for every access.WebMidiBackend(sysEx: true)asks for System Exclusive, a stronger permission to "control and reprogram" MIDI devices- In an iframe, the embedding page must allow MIDI:
<iframe allow="midi">; servers can setPermissions-Policy: midi=(self) - Web MIDI exists on the browser's main thread only, so on the Web the backend runs in the page, not in a MIDI isolate
- Bluetooth devices appear only when the operating system paired them (macOS, Windows), as ordinary ports
| Browser | Web MIDI |
|---|---|
| Chrome 43+, Edge 79+, Opera 30+ | yes |
| Firefox 108+ | yes, after a site permission add-on prompt |
| Chrome Android, Samsung Internet | yes, no Bluetooth MIDI |
| Safari, every browser on iOS | no |
| Firefox Android | no |
Installation #
dart pub add aud_midi_web
Code Examples #
import 'package:aud_midi_core/aud_midi_core.dart';
import 'package:aud_midi_standard/aud_midi_standard.dart';
import 'package:aud_midi_web/aud_midi_web.dart';
Future<void> main() async {
final backend = WebMidiBackend();
try {
await backend.start(PrintingHost());
} on MidiException catch (error) {
print(error); // e.g. the user denied the permission
return;
}
for (final port in backend.ports) {
print('${port.direction.name}: ${port.name} (${port.id})');
}
await backend.stop();
}
/// Prints what the backend reports; apps get this from aud_midi's engine.
final class PrintingHost implements MidiBackendHost {
@override
final MidiClock clock = const MidiSystemClock();
@override
void portsChanged(List<MidiPortEvent> events) => print(events);
@override
void received(MidiPortId port, MidiPacket packet) => print(packet);
@override
void diagnostic(MidiDiagnostic diagnostic) => print(diagnostic);
}
How It Works #
WebMidiApi,WebMidiAccess,WebMidiInputHandleandWebMidiOutputHandledescribe the browser API in pure Dartlib/src/browser/web_midi_browser_api.dartimplements them with package:web; a conditional import (dart.library.js_interop) picks it in the browser andWebMidiUnavailableApieverywhere elseWebMidiBackendholds all logic against these interfaces, so the Dart VM tests it with a fake;WebMidiMessageValidatorchecks bytes the wayMIDIOutput.send()does
Tests:
dart test # logic, Dart VM
dart test -P browser # adapter in Chrome, dart2js
dart test -P browser -c dart2wasm
swift tool/macos_midi_loopback.swift & # enables the loopback tests