Jeff Cole

I build things with computers, for people.

Collaborative Music Loops in Elixir and Elm

October 5, 2016

When I started learning Elixir and checking out Phoenix, one of the things that struck me was the platform's first-class support for the real-time web. Once you get a handle on Elixir and become accustomed to programming in a functional style, it's a joy to work with the abstractions that the Phoenix team has set up around sockets and channels.

I came up with an idea for a project to test drive these features. And if I was going to be using functional programming, I figured I might as well do it all the way up the stack. The Elm language stands to transform how we write client-side applications, along with how well we enjoy the experience.

In this series, I want to highlight the interesting abstractions provided by Elixir, Phoenix, and Elm that make building these types of applications much more pleasant than in the past.

The first few posts in the series will explore how Elixir and Phoenix gave me the right tools to write the back end for Loops With Friends, a collaborative music-making web app. The app supports up to seven users in a given "jam," in which each user gets their own music loop. Each user can start and stop their loop to make music in real time with the other users in the jam. The app automatically creates and balances additional jams as necessary as users join and leave.

As we go, I'll highlight the bits of code that are relevant to understanding how everything is wired together, incrementally adding lines to existing functions. Be sure to check out the full code via the links if you'd like to see the final source and tests. We won't spend much time on the details of data transformation, so if you're new to Elixir, a great place to start is the Elixir Guides.

Joining a Jam

When a player visits the app, the server sends down the client-side code, along with a jam identifier. The client-side code then immediately requests a WebSocket connection to a Phoenix channel for that identifier. The server application's Endpoint module [source] binds the /socket path to the UserSocket module.

# lib/loops_with_friends/endpoint.ex
defmodule LoopsWithFriends.Endpoint do
  use Phoenix.Endpoint, otp_app: :loops_with_friends

  socket "/socket", LoopsWithFriends.UserSocket

  # ...
end

The UserSocket module [source | test] declares the channels that are supported over the socket. The pattern "jams:*" specifies what topics requested by the client the channel will match on. Meanwhile, the connect function assigns a user ID to the socket so that we can know which user we are communicating with at all points after the initial connection.

# web/channels/user_socket.ex
defmodule LoopsWithFriends.UserSocket do
  use Phoenix.Socket

  channel "jams:*", LoopsWithFriends.JamChannel

  def connect(_params, socket) do
    {:ok, assign(socket, :user_id, UUID.uuid4())}
  end
end

Finally, the JamChannel module [source | test] implements the join function, which matches on the topic requested by the client, replies with the user's ID, and assigns the jam ID to the socket.

# web/channels/jam_channel.ex
defmodule LoopsWithFriends.JamChannel do
  use LoopsWithFriends.Web, :channel

  def join("jams:" <> jam_id, _params, socket) do
    # ...

    {:ok,
     %{user_id: socket.assigns.user_id},
     assign(socket, :jam_id, jam_id)}
  end
end

At this point, the user has successfully joined the jam — but a jam of one is a very lonely jam.

Continue to the next post to see how Phoenix Presence allows us to easily track all the players in a jam.