TuningForkMidi

MIDI ports for TuningFork — play a score or a live pattern out to a synth or a DAW, and play a keyboard into a stage.

tuning_fork itself is pure Elixir and needs none of this. Add this package only to reach a MIDI device; without it, everything else still works, including writing a .mid file with TuningFork.Midi.write!/3.

Setup

{:tuning_fork, "~> 0.1"},
{:tuning_fork_midi, "~> 0.1"}

Building needs a C compiler, and on Linux the ALSA headers (libasound2-dev).

Playing out

alias TuningFork.Midi.{Out, Port}
{:ok, [{index, name} | _rest]} = Port.outputs()
{:ok, port} = Port.open_output(index)
{:ok, playing} = Out.play(port, score, channel: 1)
Out.stop(playing)

A note becomes a note-on and a note-off; nothing is synthesised here, so whatever is listening makes the sound. Pitch becomes the nearest semitone, gain becomes velocity, and how long the voice sounds becomes the gap between the two messages. Every message is timed against one monotonic clock reading taken at the start, so a long piece does not drift.

Out.stop/1 sends every note off and lifts the sustain, so nothing is left ringing. A player whose caller dies does the same.

Out.messages/2 gives the same messages as {seconds, bytes} without a device, which is what to look at when something plays wrong.

Playing a live pattern out

import TuningFork.Pattern, only: [stack: 1]
import TuningFork.Pattern.Control
{:ok, live} = Out.pattern(port, stack([s("bd*4, hh*8"), n("0 4 7") |> scale("c:minor")]), cps: 0.5, clock: true)
Out.update_pattern(live, s("bd*2"))
Out.pattern_cps(live, 0.75)
Out.stop(live)

The player walks the pattern on a wall clock, a fraction of a second ahead, and sends each onset's note on and off at its time. Pitched values go on :channel; drum names from the kit (bd, sn, hh, cp, rd …) go on channel 10 as General MIDI percussion; a value with neither is skipped. update_pattern/3 swaps at the next cycle line, or at: :now. With clock: true the player sends MIDI start, 24 clock pulses a beat at four beats to the cycle, and stop, so a DAW can follow it. Out.pattern_messages/4 is the same translation for a window of cycles, without a device.

A port of your own

With no MIDI hardware and no loopback bus, open a virtual one. Other applications see it in their own lists and can be pointed at it:

{:ok, port} = Port.open_virtual_output("tuning_fork")

That is the way to drive a DAW or a soft synth from a laptop with nothing plugged in. Windows has no virtual ports and answers {:error, :no_backend}; a loopback driver is the way there.

Reading in

{:ok, [{index, _name} | _rest]} = Port.inputs()
{:ok, port} = Port.open_input(index, self())
:ok = Port.listen(port)
receive do
{:midi_in, ^port, bytes, monotonic_nanoseconds} -> handle(bytes)
end

One complete message per send, delivered from the backend's own thread. A port whose owner dies is closed and reclaimed rather than left open with nobody reading it. TuningFork.Midi.Port.open_virtual_input/2 is the same thing under a name of your own, for other applications to send into. TuningFork.Midi.Message.parse/1 turns the bytes into events: {:note_on, channel, note, velocity}, {:control, channel, controller, value}, {:bend, channel, amount} and the rest.

Playing a keyboard

{:ok, _stage} = TuningFork.Stage.start_link(sink: TuningFork.Sink.Speaker)
{:ok, _listener} = TuningFork.Midi.In.start_link(port: 0, voice: "gm_piano")

TuningFork.Midi.In opens an input and plays it on a stage: each key held under its own note until it is let go, the sustain pedal holding released keys until it comes up. The voice is a sound name ("gm_piano", "sawtooth", a sample bank name), a map of controls as TuningFork.Kit.voice/3 takes, or a function of note and velocity returning a TuningFork.Voice. to: pid passes every event on as {:midi, event} for an application that wants the controls and program changes too, and stage: nil does only that. Without a :port, it plays whatever {:midi_in, port, bytes, time} messages are sent to it.

Messages

TuningFork.Midi.Message builds the bytes: note_on/3, note_off/3, control/3, program/2, bend/2, hush/1. Channels are 1 to 16 as everybody counts them. Notes and velocities are clamped rather than wrapped, so a velocity worked out from a gain over 1.0 is loud rather than quiet.

Without a device

Every function answers {:error, reason} rather than raising, so a program that runs both with and without MIDI hardware does not have to branch. TuningFork.Midi.Port.available?/0 says whether the NIF loaded at all.

What this is built on

minimidio by Joseph Stewart, MIT, vendored at c_src/minimidio.h. It is the single-file C library that speaks CoreMIDI, ALSA, WinMM and Web MIDI; this package is the NIF over it.

The inbound path follows erlsci/midiio (Apache-2.0), which worked out the lifecycle a callback from a non-BEAM thread needs: keep the resource for as long as a callback might fire, build the message in a fresh process-independent environment, send it with a NULL caller env because the backend thread is not a scheduler, and monitor the owner so an abandoned port is reclaimed. Thanks to that project for doing it first.

Files, rather than devices

TuningFork.Midi.encode/2 and write!/3 in core turn a score into a Standard MIDI File, and read!/1 with to_score/2 reads one back. That path is pure Elixir and needs no device and none of this package.

Examples

mix run examples/pattern_out.exs # a live pattern out of a virtual port, with MIDI clock
mix run examples/keyboard.exs # the first input played on a stage, as a piano