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.

graph LR; A[Elixir] B[amy-message] A--"v5w7p60"-->B;

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, ": "}}
graph LR; A[Elixir] B[amy-message] B--": "-->A;

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…