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

Connect sapf to elixir with Midiex

livebooks/sapf_test.livemd

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)