Connect sapf to elixir with Midiex
Mix.install([
{:music_build, github: "bwanab/music_build", force: true},
{:music_prims, github: "bwanab/music_prims"},
{:midifile, github: "bwanab/elixir-midifile", force: true},
{:better_weighted_random, "~> 0.1.0"},
{:midiex, "~> 0.6.3"},
{:kino, "~> 0.16.0"}
])
Section
In this book, we’re going to connect elixir to sapf.
first, create an output port
dork_port = Midiex.create_virtual_output("dork")
Now, in a terminal run sapf and execute the following command:
> midiStart
This should result in a list of midiports something like:
gMIDIClient 4457347
midi sources 1 destinations 1
MIDI Source 0 ‘dork’, ‘dork’ UID: 356092455
MIDI Destination 0 ‘FluidSynth virtual port (36417)’, ‘FluidSynth virtual port (36417)’ UID: -971557172
Now, again in the sapf session, given the above output:
> 356092455 0 midiConnectInput
Finally, set debug on in sapf
> 1 midiDebug
Midiex.send_msg(dork_port, IO.iodata_to_binary([144, 72, 114]))
In the sapf terminal, you should now see:
> midi note on 0 1 72 114
- the interpretation of this is 0 -> sourceIndex, 1 -> channel (1 based), 72 - note, 114 - velocity
Now, in sapf:
> 0 1 mlastkey > –> #[72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 …] > > 0 1 mlastkey1 > –> 72
Midiex.send_msg(dork_port, IO.iodata_to_binary([145, 72, 114]))
In sapf:
> midi note on 0 2 72 114
> 0 2 mlastkey1 –> 72
Now, let’s try a control message:
Midiex.send_msg(dork_port, IO.iodata_to_binary([176, 11, 80]))
In sapf:
> midi control 0 1 11 120
> 0 1 11 0 10 mctl1 > –> 9.44882
- the interpretation of the command is 0 -> sourceIndex, 1 -> channel (1 based), 11 -> the control message we’re interested in, 0, 10, the range that the value (120) is scaled to. Thus, 120 (the midi value received) / 127 =~ 0.944882.
120 / 127
nnhz is the function that translates a midi note to a hertz signal.
in sapf:
> 0 1 mlastkey nnhz 0 sinosc .3 * play
Now, send it a note.
Midiex.send_msg(dork_port, IO.iodata_to_binary([144, 72, 114]))
You should hear the note. Send another:
Midiex.send_msg(dork_port, IO.iodata_to_binary([144, 60, 50]))
The note you hear should now be lower.
Likewise, one can detect velocity with mlastvel which works like mctl:
> 0 1 0 1 mlastvel
-
the interpretation is 0 –> sourceIndex, 1 –> channel, 0, 1 –> the range that the midi velocity value will be scaled to. Thus, we might have:
-
In sapf:
> 0 1 mlastkey nnhz 0 sinosc 0 1 0 1 mlastvel * play
Send a note at a given velocity.
Midiex.send_msg(dork_port, IO.iodata_to_binary([144, 60, 50]))
Now, change the velocity:
Midiex.send_msg(dork_port, IO.iodata_to_binary([144, 60, 30]))
Now, the sound should be lower.
Midiex.send_msg(dork_port, IO.iodata_to_binary([144, 60, 1]))
Now, it is effective off, even though sending a new velocity would show that it’s still running, just very low volume.
Now, let’s try something really crazy.
File.cd(Path.expand("~/src/music_build"))
seq = Midifile.read("midi/dork.mid")
MidiPlayer.play(seq, synth: dork_port)
You should have heard the entire midi file played! It won’t have sounded good since it’s just a simple sine wave synth, like a 1980s Atari game, but you should have heard the notes.
Now, let’s test midi control message handling. In sapf create the following:
{
:low -1 ; low number to scale pb values
:high 7 ; high number to scale bp values
:channel 1 ; channel 0 in 0 based
:middle 3.18 ; empirically determined middle value
:out \o[
0 o.channel o.low o.high mbend o.middle -
]
} = mbs
{
:low 0
:high 10
:channel 1
:control 2
:out \o[
0 o.channel o.control o.low o.high mctl
]
} = breath_ctl
0 1 mlastkey mbs.out + nnhz 0 lfsaw 0 1 0 1 mlastvel breath_ctl.out play
In elixir, do the following:
controllers = Enum.map(1..127, fn n -> [Controller.new(2, n, 0), Rest.new(0.125)] end)
|> List.flatten()
note = Note.new(:C, octave: 3, duration: 2, velocity: 80, channel: 0)
sonorities = [note| controllers ++ Enum.reverse(controllers)]
track = MusicBuild.TrackBuilder.new("dork", sonorities, 960, 0)
seq = Midifile.Sequence.new("dork", 100, [track], 960)
Then, send the events to sapf. You should hear a note starting out low volume and increasing, then decreasing back to 0.
MidiPlayer.play(seq, synth: dork_port)