OSCx tour
Mix.install([{:oscx, "~> 0.1.1"}])
Setup
Create aliases for Message, Encoder and Decoder.
alias OSCx.{Message, Encoder, Decoder}
[OSCx.Message, OSCx.Encoder, OSCx.Decoder]
Create an OSC message
An OSC message is defined using the %OSCx.Message{}
struct.
You can use the OSCx.Message.new/1
function, e.g.:
Message.new(address: "/status", arguments: ["my string argument"])
or populate the struct directly, see the cell below:
msg = %Message{address: "/status", arguments: []}
%OSCx.Message{address: "/status", arguments: []}
Encode the message
The OSCx.encode/1
function can be used to encode the struct to an OSC binary message.
encoded_osc_msg = OSCx.encode(msg)
<<47, 115, 116, 97, 116, 117, 115, 0, 44, 0, 0, 0>>
The message above is now ready to be sent via UDP.
Send an encoded message using UDP
Download Protokol
For this part, you may want to download an OSC data inspection tool, like Protokol.
Protokol is a free responsive console app for monitoring and logging control protocols, such as OSC. It can log MIDI and Gamepad inputs.
It’s available on Mac, Windows and Linux.
We’ll use Protokol to act as the OSC server (recipient) of our message.
Enable OSC in Protokol
Once Protokol is installed, load it and click the ‘OSC’ tab. Make sure Enabled is checked on.
Open a UDP port
Erlang and therefore Elixir comes with the :gen_udp
library for opening UDP ports and sending and receiving messages.
Lets use it to send an OSC message to Protokol. By default, Protokol listens on port 8000
but you can change this if necessary.
Assuming Protokol is running on the same machine as this Livebook, we’ll use the localhost IP address of 127.0.0.1
(or you can change this to 'localhost'
if you prefer).
ip_address = ~c"127.0.0.1"
port_num = 8000
# Open a port
{:ok, port} = :gen_udp.open(0, [:binary, {:active, true}])
{:ok, #Port<0.8>}
Now the port is open, lets send our message using :gen_udp.send/4
as below:
:gen_udp.send(port, ip_address, port_num, encoded_osc_msg)
:ok
You should now see our message with "/status"
on Protokol’s OSC tab:
To make it more interesting, lets send a more complex message by adding arguments to the %OSCx.Messages{arguments: []}
key like this:
encoded_osc_msg =
%Message{
address: "/some/address",
arguments: [1, 2.0, [:A, :B, :C], "Hello world", true, false, nil, :impulse]
}
|> OSCx.encode()
<<47, 115, 111, 109, 101, 47, 97, 100, 100, 114, 101, 115, 115, 0, 0, 0, 44, 105, 102, 91, 83, 83,
83, 93, 115, 84, 70, 78, 73, 0, 0, 0, 0, 0, 0, 1, 64, 0, 0, 0, 65, 0, 0, 0, 66, 0, 0, 0, 67, 0,
...>>
This message has a mix of OSC types and values:
OSC type | Elixir type | Elixir example |
---|---|---|
Integer | Integer |
1 |
Float | Float |
2.0 |
Array | List |
[1, 2, 3] |
String | String |
"Hello world" |
Symbol | Atom |
:A |
True | true |
true |
False | false |
false |
Null | nil |
nil or :null |
Impulse |
Atom (of value :impulse ) |
:impulse |
Let’s send it as before:
:gen_udp.send(port, ip_address, port_num, encoded_osc_msg)
:ok
You should now see our richer OSC message on the second line in Protokol: Unfortunately Protokol doesn’t show the array of symbols. This is something you may come across in OSC. Some types are optional parts of the OSC standard and are not implemented on all software.
Examples of some optional or non-standard types are:
- Symbols (e.g. Atoms in Elixir)
- 64-bit integers or floats
- Single ASCII character
- RGBA colour
- MIDI message
- Array (e.g. List in Elixir)
You can send an OSC array of other data instead:
bin_osc_msg =
%Message{
address: "/some/address",
arguments: [1, 2, 3, ["X", "Y", "Z"]]
}
|> OSCx.encode()
:gen_udp.send(port, ip_address, port_num, bin_osc_msg)
:ok
You can see on the third line that the list values in the last was also received:
MIDI messages via OSC can also be sent by wrapping the binary MIDI message in a %{midi: value}
map as follows:
bin_osc_msg =
%Message{
address: "/some/address",
arguments: [%{midi: <<0x90, 60, 127>>}]
}
|> OSCx.encode()
:gen_udp.send(port, ip_address, port_num, bin_osc_msg)
:ok
You should see the MIDI message type and data appear in the Protokol screen:
The OSC Spec requires a 4-byte MIDI message, defined as: port id, status byte, data1, and data2.
However, MIDI messages are predominatly consist of 3-bytes (a status byte, followed by 2 data bytes). If a 3 byte MIDI message is given, OSCx will prepend a port ID of <<0>>
to make up the 1 byte difference.
If the OSC server (receiver) supports it (Protokol doesn’t), RGBA colours can be sent as follows:
bin_osc_msg =
%Message{
address: "/some/address",
arguments: [%{rgba: [153, 234, 69, 1]}]
}
|> OSCx.encode()
:gen_udp.send(port, ip_address, port_num, bin_osc_msg)
:ok
Receiving OSC messages
Using OSC to send and receive messages from audio software is a common use-case.
This example assumes SuperCollider is installed and running on the same machine as this Livebook.
Install SuperCollider
In this example we’ll use SuperCollider. SuperCollider is an ‘audio synthesis and algorithmic composition platform’. It’s used by musicials, artists and researchers working with sound.
We can interact with SuperCollider using OSC to produce audio.
SuperCollider is available on a range of platforms: https://supercollider.github.io/downloads
Boot SuperCollider
Launch SuperCollider, and from it’s menu, select Server > Boot Server for it to boot.
Create a GenServer
SuperCollider listents to UDP port 57110
by default. We’ll use the GenServer’s state to hold the UDP socket, and implement a handle_info
callback where we’ll receive UDP messages from SuperCollider.
defmodule SC do
use GenServer
@impl true
def init(_state) do
# Open a port and add the UDP socket to the state
{:ok, socket} = :gen_udp.open(0, [:binary, {:active, true}])
{:ok, socket}
end
@impl true
def handle_cast({:send, osc_bin_msg}, state) do
# This could be changed to named address, like 'localhost'
ip_address = ~c"localhost"
sc_port_num = 57110
:gen_udp.send(state, ip_address, sc_port_num, osc_bin_msg)
{:noreply, state}
end
@impl true
def handle_info(msg, state) do
case msg do
{:udp, _process_port, _ip_addr, _port_num, res} ->
IO.inspect(res, label: "Binary message received")
IO.inspect(Message.decode(res), label: "\nDecoded message")
state
_ ->
state
end
{:noreply, state}
end
def start_link() do
GenServer.start_link(SC, nil)
end
def send(pid, osc_bin_msg) do
GenServer.cast(pid, {:send, osc_bin_msg})
end
end
{:module, SC, <<70, 79, 82, 49, 0, 0, 20, ...>>, {:send, 2}}
Let’s start our GenServer so it’s ready to send and receive messages:
{:ok, sc_pid} = SC.start_link()
{:ok, #PID<0.272.0>}
Let’s create a simple message and watch for a response.
One of the simplest messages to SuperCollider is sending a message with the address "/status"
and no arguments:
encoded_osc_msg =
%Message{address: "/status"}
|> OSCx.encode()
SC.send(sc_pid, encoded_osc_msg)
:ok
Binary message received: <<47, 115, 116, 97, 116, 117, 115, 46, 114, 101, 112, 108, 121, 0, 0, 0, 44,
105, 105, 105, 105, 105, 102, 102, 100, 100, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 111, 60, 201, ...>>
Decoded message: %OSCx.Message{
address: "/status.reply",
arguments: [1, 0, 0, 2, 111, 0.024587105959653854, 0.06639117002487183,
44100.0, 44099.94006284866]
}
If successfull, you’ll see from IO.inspect
the binary OSC message received e.g. <<47, 115, 116...>>
and it decoded as a struct:
%OSCx.Message{
address: "/status.reply",
arguments: [1, 0, 0, 2, 111, 0.029667392373085022, 0.14714068174362183]
}
Address
Notice how SuperCollider sends the same address back as we sent but with “.reply” appended to it: "/status.reply"
. When sending commands to SuperCollider, replies will often follow this convention.
Arguments
The arguments have meaning in SuperCollider, but you can see a mixture of integers and floats were returned.
Let’s try another simple message, this time we’ll send a message with the address "/version"
and no arguments:
encoded_osc_msg =
%Message{address: "/version"}
|> OSCx.encode()
SC.send(sc_pid, encoded_osc_msg)
:ok
Binary message received: <<47, 118, 101, 114, 115, 105, 111, 110, 46, 114, 101, 112, 108, 121, 0, 0, 44,
115, 105, 105, 115, 115, 115, 0, 115, 99, 115, 121, 110, 116, 104, 0, 0, 0, 0,
3, 0, 0, 0, 13, 46, 48, 0, 0, 86, 101, 114, 115, 105, 111, ...>>
Decoded message: %OSCx.Message{
address: "/version.reply",
arguments: ["scsynth", 3, 13, ".0", "Version-3.13.0", "3188503"]
}
In this case, you can see version information sent back in the decoded message arguments:
%OSCx.Message{
address: "/version.reply",
arguments: ["scsynth", 3, 13, ".0", "Version-3.13.0", "3188503"]
}
You message may be slightly different to this, depending on what you’ve installed locally.
Play a sound
You can also try asking SuperCollider to play some audio:
# Send a message to play the default sound with a new synth
synth_definition_name = "default"
synth_node_id = 400
add_action = 0
add_target_id = 0
encoded_osc_msg =
%Message{
address: "/s_new",
arguments: [synth_definition_name, synth_node_id, add_action, add_target_id]
}
|> OSCx.encode()
SC.send(sc_pid, encoded_osc_msg)
:ok
SuperCollider’s default tone should be playing.
Stop the sound
To stop the sound, evaluate the next cell:
encoded_osc_msg =
%Message{
address: "/n_free",
arguments: [synth_node_id]
}
|> OSCx.encode()
SC.send(sc_pid, encoded_osc_msg)
:ok
Wrap up
This ends the simple tour of the OSCx library.
In summary:
-
Create OSCx messages by using the
%OSCx.Message{}
struct -
Encode them with
OSCx.encode/1
-
You can send and receive OSC messages via UDP, using Erlang’s
:gen_utp
library. -
When receiving messages, as with our SuperCollider example above, use the
OSCx.decode/1
function and it will parse the binary OSC message and populate the%OSCx.Message{}
struct for you.