Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

MIDIex notebook

livebook/midiex_notebook.livemd

MIDIex notebook

Mix.install([{:midiex, "~> 0.6.3"}])
* Getting midiex (https://github.com/haubie/midiex.git)
remote: Enumerating objects: 717, done.
remote: Counting objects: 100% (174/174), done.
remote: Compressing objects: 100% (87/87), done.
remote: Total 717 (delta 79), reused 139 (delta 52), pack-reused 543
origin/HEAD set to main
Resolving Hex dependencies...
Resolution completed in 0.257s
New:
  jason 1.4.1
  rustler 0.26.0
  toml 0.7.0
* Getting rustler (Hex package)
* Getting jason (Hex package)
* Getting toml (Hex package)
==> toml
Compiling 10 files (.ex)
Generated toml app
==> jason
Compiling 10 files (.ex)
Generated jason app
==> rustler
Compiling 7 files (.ex)
Generated rustler app
==> midiex
Compiling 10 files (.ex)
Compiling crate midiex in release mode (native/midiex)
   Compiling memchr v2.5.0
   Compiling core-foundation-sys v0.8.3
   Compiling libc v0.2.138
   Compiling proc-macro2 v1.0.63
   Compiling unicode-ident v1.0.5
   Compiling coremidi-sys v3.1.0
   Compiling quote v1.0.28
   Compiling regex-syntax v0.6.28
   Compiling void v1.0.2
   Compiling heck v0.4.0
   Compiling rustler v0.29.0
   Compiling lazy_static v1.4.0
   Compiling unreachable v1.0.0
   Compiling block v0.1.6
   Compiling bitflags v1.3.2
   Compiling aho-corasick v0.7.20
   Compiling regex v1.7.0
   Compiling rustler_sys v2.3.0
   Compiling core-foundation v0.9.3
   Compiling coremidi v0.6.0
   Compiling coremidi v0.7.0
   Compiling midir v0.9.1
   Compiling syn v2.0.22
   Compiling rustler_codegen v0.29.0
   Compiling midiex v0.1.0 (/Users/haubie/Library/Caches/mix/installs/elixir-1.15.2-erts-14.0.2/56c7aef4f3fb67cf9f7eb75d4a18b401/deps/midiex/native/midiex)
    Finished release [optimized] target(s) in 6.13s
Generated midiex app
:ok

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.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 devices (ports)

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.

Midiex.hotplug()
:ok

How many ports are there?

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

List devices (ports)

ports = Midiex.ports()
[
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.34604515.3318349847.1170>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.34604515.3318349847.1171>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.34604515.3318349847.1172>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "My Out",
    num: 3,
    port_ref: #Reference<0.34604515.3318349847.1173>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.34604515.3318349847.1174>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.34604515.3318349847.1175>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.34604515.3318349847.1176>
  }
]

Filter to show input or output ports

Midiex.ports(:input)
[
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.34604515.3318349847.1177>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.34604515.3318349847.1178>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.34604515.3318349847.1179>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "My Out",
    num: 3,
    port_ref: #Reference<0.34604515.3318349847.1180>
  }
]
Midiex.ports(:output)
[
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.34604515.3318349847.1188>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.34604515.3318349847.1189>
  },
  %Midiex.MidiPort{
    direction: :output,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.34604515.3318349847.1190>
  }
]

Filtering ports by name

You can include a regular expression as the first parameter to search for matching ports.

For example, to get the output ports from any Arturia device plugged into your system, you could do the following:

Midiex.ports(~r/Arturia/, :output)
[]

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.34604515.3318349847.1198>
  }
]

Connecting to output devices

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

# Get the port
out_port = Midiex.ports(~r/Arturia/, :output) |> List.first()
nil
# Make a connection
out_conn = Midiex.open(out_port)

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

Midiex.send_msg(out_conn, <<0x90, 60, 127>>)
error: undefined variable "out_conn"
  livebook/midiex_notebook.livemd#cell:752gysgdtdthfmkq2ehlkt5iw5bnzkyw:1

Virtual devices

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

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

virtual_conn = Midiex.create_virtual_output("My Virtual Connection")
%Midiex.OutConn{
  conn_ref: #Reference<0.34604515.3318349831.2664>,
  name: "My Virtual Connection",
  port_num: 4
}

Note that although you’ve created this virtual output, on your system it will appear as an import port to be discoverable by other MIDI software or devices.

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

ports = Midiex.ports(:input)
[
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 1",
    num: 0,
    port_ref: #Reference<0.34604515.3318349847.1212>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 2",
    num: 1,
    port_ref: #Reference<0.34604515.3318349847.1213>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "IAC Driver Bus 3",
    num: 2,
    port_ref: #Reference<0.34604515.3318349847.1214>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "My Out",
    num: 3,
    port_ref: #Reference<0.34604515.3318349847.1215>
  },
  %Midiex.MidiPort{
    direction: :input,
    name: "My Virtual Connection",
    num: 4,
    port_ref: #Reference<0.34604515.3318349847.1216>
  }
]