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
:inputor:outputport - 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:
- Midiex.Message: https://midiex.hexdocs.pm/Midiex.Message.html
- MIDI Association - About MIDI-Part 3 - MIDI Messages: https://midi.org/about-midi-part-3midi-messages
- MIDI Association - Reference tables: https://midi.org/spec-detail
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.Messageis 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.Messagehelpers: While raw MIDI binaries are 0-indexed (0 to 15),Midiex.Messagehandles this conversion for you automatically. When using thechannel: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:
- Play a step-by-step major scale arpeggio.
- Trigger a long, sustained note and actively modulate its Pan, Pitch Bend, and Volume.
-
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:
-
through a low-level
Midiex.subscribe/1approach where your current process catches messages directly in a receive block -
using the
Midiex.ListenerGenServer 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.NotifierGenServer.
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