Creating a simple ping pong server in Elixir using OSC

this is a simple osc ping pong server implement in Elixir and Sonic pi just
for the sake of exploring and learning, there are a bunch of thing that I do not
know and of course there is room for improvement always, feel free to share your though

# mix.exs
add the next to dependencies
  defp deps do
    [
      {:ex_osc, "~> 0.1.1"}
    ]
  end

Server code

defmodule OscElixir do
  @moduledoc """
  Documentation for `OscElixir`.
  """
  use GenStage

  def start_link() do
    {:ok, client} = ExOSC.Client.start_link(ip: {127, 0, 0, 1}, port: 4560)
    {:ok, consumer} = GenStage.start_link(__MODULE__, client, name: __MODULE__)
    {:ok, _} = GenStage.sync_subscribe(consumer, to: client)
  end

  @impl true
  def init(client) do
    {:consumer, %{client: client}}
  end

  def send_msg() do
    GenStage.call(__MODULE__, :test)
  end

  # -- Server side
  @impl true
  def handle_call(:test, _from, %{client: client} = state) do
    ExOSC.Client.send_message(client, %OSC.Message{path: "/ping", args: [50, 100, 1]})
    {:reply, :ok, [], state}
  end

  @impl true
  def handle_events(events, {_, _}, %{client: client} = state) do


    events
    |> Enum.each(&IO.inspect/1)

    # Wait 5 second before reply
    Process.sleep(5000)
    ExOSC.Client.send_message(client, %OSC.Message{path: "/pong", args: [50, 100, 1]})

    {:noreply, [], state}
  end

end

Sonic pi code that will listen and send a reply to the server, leave it run it also


##| port = ##| we do not yet from where we are going to see a reply 
##| use_osc "localhost", port

live_loop :ping do
  use_real_time
  a, b, c = sync "/osc*/ping"
  play 60
  osc "/pong"
end

live_loop :pong do
  use_real_time
  a, b, c = sync "/osc*/pong"
  play 65
end

then we can run the server using iex -S mix
we will enter in something call interactive shell, from there we can call function,

iex(1)> OscElixir.start_link
	{:ok, #Reference<0.1.2.3>}
iex(2)> OscElixir.send_msg
	:ok

We should here a ping msg sound, and if we inspect the cues console in sonic pi we should see something like

/osc:127.0.0.1:12345/ping [50, 100, 1]

we should grab that 12345 and replace it as the port in the sonic pi code


port = 12345
use_osc "localhost", port

now if we run the elixir commands again we should see something like

iex(5)> OscElixir.send_msg
	:ok
	%OSC.Message{path: "/pong", args: []}

if we see the OSC.Message means that our conection is working! also I just decide to reply with pong
to hear confirm with audio that is working

Happy Coding!

1 Like

Nice little project. I tweaked a couple of things.
First on the elixir side, I thought the Process.sleep(5000) seemed rather long, and tried shorter values. 500 worked nicely on my Mac, although you can also get a nice chord omitting the line completely, when the response to “pong” being sent is returned pretty well instantly.

On the Sonic Pi side, I extracted the port number on which to send “pong” automatically by tweaking the routine parse_sync_address which I have used many times for extracting the values matched in osc calles with wild card data. It gives a rather horrendous extraction, but works! It uses the undocumented call get_event(address)

Ocde is:

define :get_port do |address|
  v= get_event(address).to_s.split(",")[6]
  if v != nil
    return v[3..-2].split("/")[0].split(":")[2].to_i
  else
    return ["error"]
  end
end

live_loop :ping do
  use_real_time
  a, b, c = sync "/osc*/ping"
  
  port= get_port("/osc*/ping")
  puts "sending to port #{port}"
  play 60
  osc_send "127.0.0.1",port, "/pong"
end

live_loop :pong do
  use_real_time
  a, b, c = sync "/osc*/pong"
  puts "Received #{a},#{b},#{c}"
  play 65
end
1 Like