MIDI in + out for Mob apps.
Receive from a controller (keyboard, pads) and send to external synths / DAWs,
over USB-MIDI and BLE-MIDI. Unlike MobBluetooth (classic BT, Android
only), MIDI is first-class on both platforms, so this is a real cross-platform
surface: CoreMIDI on iOS, android.media.midi on Android.
API style
Same as the rest of Mob: callbacks take and return socket unchanged; results
arrive in handle_info/2 as messages tagged :midi. Functions return
{:error, :unsupported} on the host (no MIDI stack outside a device).
Discovering devices
MobMidi.list_devices(socket)
# => {:midi, :devices, [%{id: 1, name: "Oxygen 49", direction: :input}, ...]}Hot-plug: {:midi, :device_added, device} / {:midi, :device_removed, id}.
Receiving
MobMidi.open_input(socket, device_id) # subscribe to that sourceIncoming MIDI is delivered to the calling process as raw packets:
{:midi, :raw, %{device: id, bytes: <<0x90, 60, 100>>}}Parse it with parse/1 (pure, so it's unit-testable and you decide how much
to decode):
def handle_info({:midi, :raw, %{bytes: b}}, socket) do
for ev <- MobMidi.parse(b), do: react(ev)
{:noreply, socket}
endparse/1 returns events like %{type: :note_on, channel: 0, note: 60, velocity: 100} (:note_on with velocity 0 is normalised to :note_off),
:note_off, :cc, :program_change, :pitch_bend, and %{type: :raw, bytes: ...} for anything it doesn't decode.
Sending
MobMidi.open_output(socket, device_id)
MobMidi.send_note_on(socket, device_id, 0, 60, 100) # ch 0, middle C, vel 100
MobMidi.send_note_off(socket, device_id, 0, 60, 0)
MobMidi.send_cc(socket, device_id, 0, 7, 90) # ch 0, CC#7 (volume)
MobMidi.send_raw(socket, device_id, <<0xF0, ...>>) # SysEx / anythingChannels are 0..15 on the wire (shown as 1..16 in most UIs). Notes /
velocities / values are 0..127.
Summary
Functions
Close any open input/output for device_id.
Enumerate available MIDI devices. Result: {:midi, :devices, [device]}.
Subscribe to a device's MIDI input. Incoming packets arrive as
{:midi, :raw, %{device: id, bytes: binary}} to the calling process.
Open a device's MIDI output so send_* can write to it.
Parse a raw MIDI byte stream into a list of events. Pure — handles the common
channel-voice messages (note on/off, CC, program change, pitch bend) and emits
%{type: :raw, bytes: ...} for one byte at a time when it can't decode (SysEx,
running status, real-time bytes). Velocity-0 Note On is normalised to
:note_off.
Normalise a {:midi, :devices, payload} payload to a list of
%{id: integer, name: binary, direction: :input | :output | :both}.
Send a Control Change (CC).
Send a Note Off.
Send a Note On.
Send a Program Change.
Send raw MIDI bytes (SysEx, or anything send_* doesn't cover).
Types
Functions
Close any open input/output for device_id.
Enumerate available MIDI devices. Result: {:midi, :devices, [device]}.
Subscribe to a device's MIDI input. Incoming packets arrive as
{:midi, :raw, %{device: id, bytes: binary}} to the calling process.
Open a device's MIDI output so send_* can write to it.
Parse a raw MIDI byte stream into a list of events. Pure — handles the common
channel-voice messages (note on/off, CC, program change, pitch bend) and emits
%{type: :raw, bytes: ...} for one byte at a time when it can't decode (SysEx,
running status, real-time bytes). Velocity-0 Note On is normalised to
:note_off.
Normalise a {:midi, :devices, payload} payload to a list of
%{id: integer, name: binary, direction: :input | :output | :both}.
iOS delivers a list of maps directly; Android delivers a JSON string (the
Kotlin bridge builds it from MidiDeviceInfo). This collapses both, so a
screen can do payload |> MobMidi.parse_devices() |> Enum.filter(...). Pure.
Send a Control Change (CC).
Send a Note Off.
Send a Note On.
Send a Program Change.
Send raw MIDI bytes (SysEx, or anything send_* doesn't cover).