MIDI¶
IMIDIManager provides basic MIDI input and output through AppContext.
It is available when SNOWPULSE_HAS_MIDI=1; the web PLAYABLE_AD profile
compiles the service out.
Supported v1 messages are channel voice messages:
- note off / note on
- polyphonic pressure
- control change
- program change
- channel pressure
- pitch bend
SysEx and MIDI system messages are intentionally rejected.
Access¶
Web builds must request MIDI access from game code, ideally from a user gesture such as a button click. App::init() does not request access automatically.
auto* midi = context()->midiManager();
if (midi) {
midi->requestAccess([](snowpulse::MIDIAccessStatus status) {
if (status == snowpulse::MIDIAccessStatus::Granted) {
// MIDI is ready.
}
});
}
On web, MIDI requires browser WebMIDI support and a secure context (https:// or localhost). The engine requests navigator.requestMIDIAccess({ sysex: false }).
On macOS desktop, Snowpulse uses CoreMIDI. On Windows desktop, iOS, and
Android, the default manager reports MIDIAccessStatus::Unavailable.
The full platform matrix is:
| Platform | Backend |
|---|---|
| macOS desktop | CoreMIDI input/output and device notifications |
| Windows desktop | Unavailable manager |
Web (FULL) |
Web MIDI API with sysex: false |
Web (PLAYABLE_AD) |
Compiled out |
| iOS | Unavailable manager |
| Android | Unavailable manager |
Input¶
Register onMessage() before or after access. Incoming messages are queued by the platform backend and dispatched during IMIDIManager::update(), which App calls once per frame.
auto* midi = context()->midiManager();
midi->onMessage([](const snowpulse::MIDIMessage& message) {
if (message.type() == snowpulse::MIDIMessageType::NoteOn) {
const int channel = message.channel(); // zero-based, 0..15
const int note = message.data1();
const int velocity = message.data2();
(void)channel;
(void)note;
(void)velocity;
}
});
MIDIMessage::bytes stores the raw MIDI bytes. Invalid, incomplete, system, and SysEx messages are not dispatched.
deviceId identifies the source input and timestampSeconds carries the
platform timestamp when one is available.
Immediate Input¶
onMessageImmediate() bypasses the per-frame queue for latency-sensitive work
such as triggering a sound from a drum controller:
midi->onMessageImmediate([audio](const snowpulse::MIDIMessage& message) {
if (message.type() == snowpulse::MIDIMessageType::NoteOn) {
// Keep this callback thread-safe. Do not touch Scene, Node, or UI state.
audio->play("audio/hit.wav");
}
});
On CoreMIDI this callback runs on the platform MIDI callback thread. The
handler must be thread-safe and must not access the scene graph, UI, or other
main-thread-only state. Use normal onMessage() for gameplay changes. The web
backend invokes the immediate path from its browser callback before queueing
the same message for normal per-frame delivery.
Devices¶
Use inputs() and outputs() after access to inspect connected devices. Register onDevicesChanged() to handle browser statechange events or CoreMIDI device changes.
midi->onDevicesChanged([](const std::vector<snowpulse::MIDIDeviceInfo>& inputs,
const std::vector<snowpulse::MIDIDeviceInfo>& outputs) {
printf("MIDI inputs: %zu, outputs: %zu\n", inputs.size(), outputs.size());
});
Each MIDIDeviceInfo includes an id, name, manufacturer, and connected flag.
Output¶
Send a valid basic MIDI message to a connected output id.
snowpulse::MIDIMessage noteOn;
noteOn.bytes = { 0x90, 60, 100 }; // note on, channel 0, middle C
midi->send(midi->outputs().front().id, noteOn);
send() returns false if access is not granted, the output id is missing, or the message is not a valid supported channel voice message.