Elixir and Ports
Last week I wrote about using Elixir Ports to talk to the AMY sound engine.
You can read about that here,
but as a short recap,
Ports allow Elixir to run and talk to external programs
such as amy-message
through standard input and output channels.
With a Port created as p
, this code:
send(p, {self(), {:command, "v5w7p60"}})
..will write v5w7p60
to the input channel of amy-message
.
This week, I’ll focus on getting responses from amy-message
that we can use in
our code.
The Port module uses Elixir’s mailboxes to communicate with programs it starts.
This was demonstrated in the use of the send
function above.
To get something back from a Port, we need to use the complimentary receive
function, as:
iex(9)> r = receive do msg -> msg end
{#Port<0.6>, {:data, ": "}}
In this example, the resulting r
is a tuple containing
values telling us where the message came from and a tuple
containing an atom :data
and the actual data ": "
sent by amy-message
.
We can use pattern-matching to extract the actual data from this tuple like:
iex(10)> {_, {:data, s}} = r
{#Port<0.6>, {:data, ": "}}
iex(11)> s
": "
FYI, s
contains ": "
, which is the prompt string amy-message
wrote to indicate
it’s waiting for a new incoming command string.
There’s one thing to be cautious about with receive
though.
If for some reason amy-message
doesn’t
send a response, receive
will wait forever for a response, making Elixir’s
iex
appear to lock up (although the BEAM-VM is actually running just fine).
To safeguard against this, use after
with the receive
function and specify
the number of milliseconds to wait for a message, like this:
iex(12)> r = receive do msg -> msg after 1_000 -> :timeout end
:timeout
…here we see the :timeout
atom was returned after 1,000 milliseconds of nothing
in the mailbox.
To make the timeout condition easier to work with,
change the return structure to mimic a normal mailbox message by returning
a tuple containing a :timeout
atom and an empty string:
iex(13)> r = receive do msg -> msg after 1_000 -> {self(), {:timeout, ""}} end
{#PID<0.108.0>, {:timeout, ""}}
iex(14)> {_, {_, s}} = r
{#PID<0.108.0>, {:timeout, ""}}
iex(15)> s
""
Now that we have these basic elements in place, more complex
interactions with amy-message
are possible.
A simple example would be to ask amy-message
for its sysclock (
the number of milliseconds the AMY sound engine has been running) by
sending @c
, then capturing the response:
iex(3)> send(p, {self(), {:command, "@c"}})
{#PID<0.108.0>, {:command, "@c"}}
iex(4)> r = receive do msg -> msg after 1_000 -> {self(), {:timeout, ""}} end
{#Port<0.7>, {:data, "13531\n: "}}
iex(5)> {_, {:data, s}} = r
{#Port<0.7>, {:data, "13531\n: "}}
There’s a little more work to do, as s
contains two lines of text: the
sysclock number as a string, a return character \n
, and the prompt character.
We handle this by splitting s
into an array a
, using "\n"
to signal what to split on:
iex(18)> a = String.split(s, "\n")
["13531", ": "]
Then convert the sysclock element of a
into a number using a pipeline:
iex(21)> sysclock = a |> Enum.at(0) |> String.to_integer
13531
Now sysclock
is a number where can perform math functions.
Knowing sysclock inside a running AMY engine will be useful as a way to synchronize audio and events with AMY engines running on other devices.
We now have the basic structure for interacting with things started with the Port module!
One last thing to show is how to stop amy-message
in a controlled manner:
send(p, {self(), {:command, ":q"}})
…upon which, no trace should be left running.
You can verify this by:
pgrep -laf amy-message
I think we’ve covered enough for now, I certainly had to refer to the Elixir documents here to write this and hope it was useful for you.
Next time, I’ll present a module to simplify these interactions and show a very simple event sequencer.
Until then, enjoy a few live-coding sessions showing new features I’ve added to the AMY codebase:
Capture a PCM patch, modify it, and store to a user PCM patch…
Wave sequencing and the resonant low-pass filter…