MobMidi (mob_midi v0.1.0)

Copy Markdown View Source

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 source

Incoming 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}
end

parse/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 / anything

Channels 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 raw MIDI bytes (SysEx, or anything send_* doesn't cover).

Types

byte7()

@type byte7() :: 0..127

channel()

@type channel() :: 0..15

device_id()

@type device_id() :: non_neg_integer()

Functions

close(socket, device_id)

@spec close(term(), device_id()) :: term()

Close any open input/output for device_id.

list_devices(socket)

@spec list_devices(term()) :: term()

Enumerate available MIDI devices. Result: {:midi, :devices, [device]}.

open_input(socket, device_id)

@spec open_input(term(), device_id()) :: term()

Subscribe to a device's MIDI input. Incoming packets arrive as {:midi, :raw, %{device: id, bytes: binary}} to the calling process.

open_output(socket, device_id)

@spec open_output(term(), device_id()) :: term()

Open a device's MIDI output so send_* can write to it.

parse(bytes)

@spec parse(binary()) :: [map()]

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.

parse_devices(devices)

@spec parse_devices(list() | binary() | term()) :: [map()]

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_cc(socket, device_id, channel, controller, value)

@spec send_cc(term(), device_id(), channel(), byte7(), byte7()) :: term()

Send a Control Change (CC).

send_note_off(socket, device_id, channel, note, velocity)

@spec send_note_off(term(), device_id(), channel(), byte7(), byte7()) :: term()

Send a Note Off.

send_note_on(socket, device_id, channel, note, velocity)

@spec send_note_on(term(), device_id(), channel(), byte7(), byte7()) :: term()

Send a Note On.

send_program_change(socket, device_id, channel, program)

@spec send_program_change(term(), device_id(), channel(), byte7()) :: term()

Send a Program Change.

send_raw(socket, device_id, bytes)

@spec send_raw(term(), device_id(), binary()) :: term()

Send raw MIDI bytes (SysEx, or anything send_* doesn't cover).