Powered by AppSignal & Oban Pro

MIDIex notebook

livebook/midiex_notebook.livemd

MIDIex notebook

Mix.install([
  {:midiex, "~> 0.6.4"},
  {:kino, "~> 0.19.0"}
])

Introduction

Learning objectives

This will get you started with Midiex. By the end of this Livebook you’ll be able to:

  • Find and connect to MIDI ports on your system
  • Create virtual ports (on supported systems, like MacOS and Linux)
  • Send and recieve messages.

Setup

Just to make our code a bit more compact when experimenting with live-music coding, we’ll alias the Midiex.Message module as M.

Midiex function names have been kept compact as possible with live-music coding in mind.

alias Midiex.Listener
alias Midiex.Notifier
alias Midiex.Message, as: M
Midiex.Message

MIDI concepts

Skip this section you’re familiar with MIDI concepts and want to start playing with the library.

At it’s most basic, MIDI consists of:

  • Ports, which represent input or output connections to MIDI hardware or software. You can recieve MIDI messages from a MIDI input, or send MIDI messages to a MIDI output.
  • Connections, just like with all IO operations, you’ll need to make a connection with a MIDI port to send or recieve messages to it.
  • Messages, which are usually music related, such switching a note on or off.

Finding MIDI devices (ports)

Hot plugging (Apple)

Note that on Apple Mac, you may wish to call Midiex.hotplug() first so that Midiex will be able to see devices plugged in or removed. Skip this for all other platforms.

MIDI hot plugging refers to the system’s ability to instantly recognise MIDI hardware upon connection via USB or Bluetooth, as well as safely detach it upon removal, without requiring a system reboot or restarting active digital audio workstations (DAWs).

Midiex.hotplug()
:ok

How many MIDI ports are there?

To get a simple count of MIDI ports, calling Midiex.port_count() will return a map of input and output (e.g. %{input: 3, output: 3}) ports:

Midiex.port_count()
%{input: 4, output: 3}

Listing all MIDI ports

Without any parameters Midiex.ports() lists all discoverable MIDI ports (device or virtual).

ports = Midiex.ports()
[
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539416.122396>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.1738503359.1908539416.122397>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.1738503359.1908539416.122398>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "MidiKeys",
    num: 3,
    port_ref: #Reference<0.1738503359.1908539416.122399>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539416.122400>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.1738503359.1908539416.122401>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.1738503359.1908539416.122402>
  }
]

Filtering and queries

The Midiex.ports() function can filter the available MIDI ports in a number of ways:

  • Direction: which can be either an :input or :output port
  • Query: using a regular expression of the port name
  • Name: a string of the name of the port

… or even a combination of the above (see below).

Filter to show input or output ports

Provide the atoms :input or :output to filter by direction:

Midiex.ports(:input)
[
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539416.122403>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.1738503359.1908539416.122404>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.1738503359.1908539416.122405>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "MidiKeys",
    num: 3,
    port_ref: #Reference<0.1738503359.1908539416.122406>
  }
]
Midiex.ports(:output)
[
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539416.122414>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.1738503359.1908539416.122415>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.1738503359.1908539416.122416>
  }
]

Dynamic port filtering by regex or string

You can include a regular expression as the first parameter to search for matching ports by their name or description.

Optionally, you can provide a :input or :ouput direction atom as the last parameter.

⚠️ Note that port naming conventions may differ by platform (Mac, Linux and Windows) and underlying technology. For example, ALSA on Linux lists ports using the “Client:Port” format, which includes the dynamic ID numbers followed by their human-readable device names and port descriptions, where as Mac just lists their human-readable names.

To get the output ports from any ‘Arturia’ device plugged into your system, you could do the following:

Midiex.ports(~r/Arturia/, :output)
[
  %Midiex.MidiPort{
    direction: :output,
    name: "Arturia MicroFreak",
    num: 3,
    port_ref: #Reference<0.1738503359.1908539416.122425>
  }
]

If you know the name of the port, you can pass it as a string as the first parameter:

Midiex.ports("IAC Driver Bus 1", :input)
[
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539416.122426>
  }
]

Sending MIDI messages via output connections

Sending MIDI messages to an output device requires an active connection. As with other I/O, you can open and close these connections using Midiex.open/1 and Midiex.close/1.

Connecting to output devices

If you wanted to connect to the first Arturia output port on your system you could:

# Get the port - feel free to change this to a device listed on your system!
out_port = Midiex.ports(~r/Arturia/, :output) |> List.first()
%Midiex.MidiPort{
  direction: :output,
  name: "Arturia MicroFreak",
  num: 3,
  port_ref: #Reference<0.1738503359.1908539416.122443>
}
# Make a connection
out_conn = Midiex.open(out_port)
%Midiex.OutConn{
  conn_ref: #Reference<0.1738503359.1908539395.124097>,
  name: "Arturia MicroFreak",
  port_num: 3
}

You can now send a message to the device via the output connection, for example:

Midiex.send_msg(out_conn, <<0x90, 60, 127>>)
%Midiex.OutConn{
  conn_ref: #Reference<0.1738503359.1908539395.124097>,
  name: "Arturia MicroFreak",
  port_num: 3
}

Note the binary MIDI message format above: <<0x90, 60, 127>>.

New to MIDI messages?

A MIDI message is a digital, binary-coded instruction that acts as a piece of a music description language. Instead of transmitting actual audio, it communicates performance events, such as triggering a note, adjusting volume or changing instrument sounds between electronic musical instruments and devices. The general shape and size of a standard MIDI message is highly compact and follows this structure:

  • Status byte: Every message begins with an eight-bit (1-byte) status byte. This byte identifies the specific command being sent (like a “Note On” signal) and often specifies which of the 16 MIDI channels it applies to.
  • Data bytes: The status byte is generally followed by one or two data bytes. These bytes provide the exact details or values for the command, such as which musical key was pressed and how hard it was struck (velocity).

For more information, see:

Helpers to build MIDI messages

So that you don’t have to remember all the MIDI message codes, The Midiex.Message module has a large range of functions to create the required binary packets, so you don’t have to memorise status bytes or hex codes.

⚠️ Use of Midiex.Message is a completely optional convenience, and you can instead build your own MIDI messages using Elixir binaries (as above).

See the Midiex.Message docs for details.

Some MIDI message construction examples are below.

Symbolic notes

Functions accepting notes (pitch) as an input, will allow you do define it using integers, strings or atoms! In the example below, they all are equivalent and resolve to MIDI note number 60 (Middle C):

note_atom     = M.note_on(:C4)       # <<144, 60, 127>>
note_integer  = M.note_on(60)        # <<144, 60, 127>>
note_string   = M.note_on("C4")      # <<144, 60, 127>>
note_string_m = M.note_on("MiddleC") # <<144, 60, 127>>
 
IO.inspect({note_atom, note_integer, note_string, note_string_m }, label: "Note-on message:")
Note-on message:: {<<144, 60, 127>>, <<144, 60, 127>>, <<144, 60, 127>>, <<144, 60, 127>>}
{<<144, 60, 127>>, <<144, 60, 127>>, <<144, 60, 127>>, <<144, 60, 127>>}

Routing and velocity

You can also control volume/intensity with velocity (0-127) and target specific MIDI channels.

⚠️ Note on MIDI channels when using Midiex.Message helpers: While raw MIDI binaries are 0-indexed (0 to 15), Midiex.Message handles this conversion for you automatically. When using the channel: option, use standard natural channel numbers (1 to 16) directly.

funky_bass = M.note_on(:C2, 105, channel: 3) # Velocity: 105, Channel: 3

IO.inspect(funky_bass, label: "Channel 3 bass note")
Channel 3 bass note: <<146, 36, 105>>
<<146, 36, 105>>

Sequencing and modulation exercise

Let’s put those messages to work. In this exercise, we will:

  1. Play a step-by-step major scale arpeggio.
  2. Trigger a long, sustained note and actively modulate its Pan, Pitch Bend, and Volume.
  3. Safeguard our ears by cleaning up with all_notes_off/1.

Note the example below assumes you have opened connection on out_conn.

STEP 1: Play a C-major scale step-by-step

channel_num = 3 # Set the channel number of your device here. Use 1 for default.
scale = [:C4, :D4, :E4, :F4, :G4, :A4, :B4, :C5]

Enum.each(scale, fn note ->
  # Send note on
  Midiex.send_msg(out_conn, M.note_on(note, 90, channel: channel_num))
  :timer.sleep(200)

  # Send note off to keep it clean
  Midiex.send_msg(out_conn, M.note_off(note))
end)

:timer.sleep(500)
:ok

STEP 2: Play and modulate an expressive note

# Trigger a long, sustained note on G4
Midiex.send_msg(out_conn, M.note_on(:G4, 100, channel: channel_num))

# A. Sweep the Panoramic Stereo Field from Left to Right
# Uses pan/2 (0 = Hard Left, 64 = Center, 127 = Hard Right)
for p <- 0..127 do
  Midiex.send_msg(out_conn, M.pan(p, channel: channel_num))
  :timer.sleep(10)
end

# B. Push the Pitch Bend Up and Down
# Uses pitch_bend/2 (8192 is center/unbent)
for bend <- 8192..13000//100 do
  Midiex.send_msg(out_conn, M.pitch_bend(bend, channel: channel_num))
  :timer.sleep(10)
end

for bend <- 13000..8192//-100 do
  Midiex.send_msg(out_conn, M.pitch_bend(bend, channel: channel_num))
  :timer.sleep(10)
end

# C. Slowly Fade the Volume to Zero
# Uses volume/2 (0-127)
for v <- 127..0//-3 do
  Midiex.send_msg(out_conn, M.volume(v, channel: channel_num))
  :timer.sleep(20)
end
[:ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok,
 :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok,
 :ok, :ok, :ok]

STEP 3: Clean up

# Call this to stop any hanging notes.
Midiex.send_msg(out_conn, M.all_notes_off())

# Restore default pan and volume for subsequent runs
Midiex.send_msg(out_conn, M.pan(64, channel: channel_num))
Midiex.send_msg(out_conn, M.volume(100, channel: channel_num))
%Midiex.OutConn{
  conn_ref: #Reference<0.1738503359.1908539395.124097>,
  name: "Arturia MicroFreak",
  port_num: 3
}
⚠️ Troubleshooting

Note that some MIDI device’s firmware lack support for standard channel mode messages like CC 123 (all notes off) and CC 120 (all sound off). When sent, it ignores those commands entirely.

In those situations, you could either send a program change message (Midiex.Message.program_change/2 which will stop the current program on the device):

Midiex.send_msg(out_conn, M.program_change(3, channel: channel_num))

Or ‘brute force’ stopping all notes, as below:

# Sends an individual Note Off message for every single key (0 to 127) 
# on a specified MIDI channel to clear out non-responsive hardware.

channel_num = 3

for note <- 0..127 do
    message = M.note_off(note, _velocity=0, channel: channel_num)
    Midiex.send_msg(out_conn, message)
end
[
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  %Midiex.OutConn{
    conn_ref: #Reference<0.1738503359.1908539395.124097>,
    name: "Arturia MicroFreak",
    port_num: 3
  },
  ...
]

Virtual devices

A virtual MIDI device is software that acts like a physical MIDI equipment.

By using virtual ports, your Elixir application can act as a standalone software MIDI device. This lets you pass MIDI data seamlessly back and forth between Elixir and external software like Ableton Live, Logic Pro or GarageBand without needing physical hardware cables.

⚠️ Platform note: Virtual ports are supported on MacOS and Linux, but are currently unavailable on Windows due to OS-level limitations.

Virtual outputs

A virtual output creates an output connection you can send messages to from Elixir, but to other devices on your system, it will appear as a MIDI input they can consume.

graph LR
    subgraph ElixirApp [Elixir Application]
        A[Elixir Code]
    end

    subgraph MIDI_Bridge [OS MIDI Subsystem]
        B(("Virtual MIDI Port<br/>(Created as 'Output')"))
    end

    subgraph SystemDevices [Other Apps / Hardware]
        C["MIDI Input Consumer<br/>(e.g. Ableton, Logic, Synth)"]
    end

    A -->|Sends messages OUT| B
    B -->|Exposed as MIDI IN| C

    %% Styling
    style B fill:#ffd27f,stroke:#e67e22,stroke-width:2px

For example, you might have a software synth installed on your PC that can consume these MIDI messages.

Midiex.create_virtual_output/1 takes a string as its first paramater, representing the name of the port:

virtual_conn = Midiex.create_virtual_output("My Virtual Output")
%Midiex.OutConn{
  conn_ref: #Reference<0.1738503359.1908539395.124260>,
  name: "My Virtual Output",
  port_num: 5
}

Note that although you’ve created this virtual output, for other MIDI software or devices it will appear as an input to listen to e.g.:

If you call Midiex.ports/1 you’ll see it as an input:

ports = Midiex.ports(~r/My Virtual Output/)
[
  %Midiex.MidiPort{
    direction: :input,
    name: "My Virtual Output",
    num: 5,
    port_ref: #Reference<0.1738503359.1908539415.122401>
  }
]

Virtual inputs

A virtual input creates an input connection you can receive messages from in Elixir, but to other devices on your system, it will appear as a MIDI output (destination) they can send to:

graph LR
    subgraph SystemDevices [Other Apps / Hardware]
        A["MIDI Output Producer<br/>(e.g. Ableton, Keyboard)"]
    end

    subgraph MIDI_Bridge [OS MIDI Subsystem]
        B(("Virtual MIDI Port<br/>(Created as 'Input')"))
    end

    subgraph ElixirApp [Elixir Application]
        C[Elixir Code]
    end

    A -->|Sends messages OUT| B
    B -->|Delivers messages IN| C

    %% Styling
    style B fill:#a8dadc,stroke:#457b9d,stroke-width:2px
my_elixir_instrument = Midiex.create_virtual_input("My Elixir Instrument")
%Midiex.VirtualMidiPort{direction: :input, name: "My Elixir Instrument", num: 1}

Note that you subscribe to the port for it to become visible to other MIDI software.

To do this, you can pass the %Midiex.VirtualMidiPort{} struct to one of the MIDI input port listener functions, such as:

  • Midiex.subscribe(my_elixir_instrument)
  • If using a Listener GenServer, one of the following:
    • Midiex.Listener.start_link(port: my_elixir_instrument)
    • Midiex.Listener.subscribe(listener, my_elixir_instrument)

Likewise, once subscribed to, the virtual input port can be unsubscribed to:

  • Midiex.unsubscribe(my_elixir_instrument)
  • If using a Listener GenServer: Midiex.Listener.unsubscribe(listener, my_elixir_instrument)
# Start the listener and subscribe to the virtual input port
{:ok, listener} = Listener.start_link(port: my_elixir_instrument)
{:ok, #PID<0.315.0>}

It will now be visible as a destination MIDI device in software, for example:

And as an :output port when listing ports:

Midiex.ports(~r/My Elixir Instrument/)
[
  %Midiex.MidiPort{
    direction: :output,
    name: "My Elixir Instrument",
    num: 4,
    port_ref: #Reference<0.1738503359.1908539415.122416>
  }
]

Receiving MIDI messages (inputs and listeners)

Receiving MIDI messages in Midiex involves listening to an :input port and handling the data it broadcasts.

Midiex provides two ways of doing this:

  1. through a low-level Midiex.subscribe/1 approach where your current process catches messages directly in a receive block
  2. using the Midiex.Listener GenServer to route messages to callback functions.

When subscribing, they can take either a single input port or a multiple in a list [].

See the Midiex.subscribe/1 and Midiex.Listener docs for details.

Below is an example using the Midiex.Listener GenServer.

Step 1: Start the Listener GenServer

Using the Kino supervisor to start it below in Live Book.

{:ok, listener} = Kino.start_child(Listener)
{:ok, #PID<0.320.0>}

Step 2: Add handler callback functions

Listener.add_handler(listener, fn msg ->
  IO.inspect(msg, label: "🎵 MIDI Message Received")
end)
:ok

Step 3: Find or create input port(s)

This can be a physical or virtual port.

# Physical / OS port - getting the first port
input_port = Midiex.ports(:input) |> List.first()

# Alternatively, you could create a virtual input port:
# input_port = Midiex.create_virtual_input("Livebook Listener")
%Midiex.MidiPort{
  direction: :input,
  name: "IAC Driver Bus 1",
  num: 0,
  port_ref: #Reference<0.1738503359.1908539415.122417>
}

Step 4: Subscribe to the input port(s)

Listener.subscribe(listener, input_port)
:ok
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 48, 95>>,
  timestamp: 710407307842
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 0>>,
  timestamp: 710407352795
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 1>>,
  timestamp: 710407376795
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 48, 0>>,
  timestamp: 710407382140
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 50, 95>>,
  timestamp: 710407382171
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 2>>,
  timestamp: 710407400717
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 50, 0>>,
  timestamp: 710407411093
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 52, 95>>,
  timestamp: 710407411109
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 3>>,
  timestamp: 710407424613
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 52, 0>>,
  timestamp: 710407435978
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 53, 95>>,
  timestamp: 710407435994
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 4>>,
  timestamp: 710407456591
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 53, 0>>,
  timestamp: 710407461481
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 55, 95>>,
  timestamp: 710407461489
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 55, 0>>,
  timestamp: 710407485878
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 57, 95>>,
  timestamp: 710407485887
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 5>>,
  timestamp: 710407512963
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 57, 0>>,
  timestamp: 710407515177
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 59, 95>>,
  timestamp: 710407515202
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 59, 0>>,
  timestamp: 710407565279
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 60, 95>>,
  timestamp: 710407565337
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 6>>,
  timestamp: 710407592800
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 60, 0>>,
  timestamp: 710407637315
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 62, 95>>,
  timestamp: 710407637355
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 5>>,
  timestamp: 710407688689
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 4>>,
  timestamp: 710407736843
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 3>>,
  timestamp: 710407784661
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 62, 0>>,
  timestamp: 710407794511
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<144, 64, 95>>,
  timestamp: 710407794546
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<208, 0>>,
  timestamp: 710407806314
}
🎵 MIDI Message Received: %Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.1738503359.1908539415.122417>
  },
  data: <<128, 64, 0>>,
  timestamp: 710407812169
}

Notice that the MidiMessage struct includes the MIDI port, MIDI binary data and timestamp.

The port field makes it easy to pattern match against - for example, if you’re want to process ports differently in the one GenServer (or you can have multiple GenServers, one for each device, etc.):

%Midiex.MidiMessage{
  port: %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.3368456952.827457560.19372>
  },
  data: <<128, 50, 0>>,
  timestamp: 708101269796
}

Step 5: Unsubscribe to clean-up

Simply pass the input port struct you whish to unsubscribe from as follows:

Listener.unsubscribe(listener, input_port)
:ok

Device hotplug monitoring and notifications

Musicians constantly plug in keyboards, unplug synths and power cycle controllers mid-performance. To prevent your application from losing track of the system’s state, Midiex provides a dedicated monitoring service to detect these changes.

Just like handling MIDI input subscriptions, there are two ways to listen for system events:

  • Low-level subscription via Midiex.notifications/0 (which sends raw events to your current process)
  • High-level Midiex.Notifier GenServer.

In this section, we will use the Midiex.Notifier GenServer to monitor our system. It listens directly to OS-level MIDI changes, captures connect and disconnect events and broadcasts a clean %Midiex.MidiNotification{} struct to your registered handlers, allowing your application to dynamically adapt to hardware changes on the fly.

⚠️ Platform support: Notification messages and hot-plugging detection are currently implemented on macOS only. If you are running this on Linux or Windows, these specific notification APIs will not capture hardware changes.

See the Notifications and hot plugging and Midiex.Notifier docs for more information.

Step 1: Start the supervised notifier

{:ok, notifier} = Kino.start_child(Notifier)
# Midiex.hotplug()
{:ok, #PID<0.328.0>}

Step 2: Register a live notification handler

Below we’re attaching a handler to print clean, human-readable messages to the Livebook console whenever a MIDI port is added or removed from the system.

# Add a handler that matches on the incoming %Midiex.MidiNotification{} struct
Notifier.add_handler(notifier, fn notification ->
  case notification do
    %Midiex.MidiNotification{notification_type: :added, name: name} ->
      IO.puts("🔌 Device Added: #{name}")

    %Midiex.MidiNotification{notification_type: :removed, name: name} ->
      IO.puts("❌ Device Removed: #{name}")

    other ->
      IO.puts("ℹ️ MIDI System Event: #{inspect(other)}")
  end
end)

IO.puts("📢 Hotplug monitoring active! Try plugging or unplugging a USB MIDI device now.")
📢 Hotplug monitoring active! Try plugging or unplugging a USB MIDI device now.
:ok
❌ Device Removed: Arturia MicroFreak
❌ Device Removed: Arturia MicroFreak
🔌 Device Added: Arturia MicroFreak
🔌 Device Added: Arturia MicroFreak
❌ Device Removed: Arturia MicroFreak
❌ Device Removed: Arturia MicroFreak