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