[062] Quickie Synth

Using processes, gs, and shelling out to `sox` to build an Elixir-based synthesizer module.

Subscribe now

Quickie Synth [05.13.2016]

In today's episode, we're going to build a quickie synth. We're going to do this by shelling out to the sox program, which is available on Windows, OS X, and Linux. Let's get started.

Project

We'll get started with mix new quickie_synth and then cd quickie_synth.

Now, before we actually get into it, let me show you what we're going to use sox for. Type the following into your console:

play -qn synth 2 pluck C

If you've got sox installed, then you just heard a guitar pluck a C note. We're going to build a system to produce those shell commands based on input from the keyboard. Enough chat!

Sound

So let's build our first module. This is going to be the Sound module. mkdir test/quickie_synth and open up test/quickie_synth/sound_test.exs:

defmodule QuickieSynth.SoundTest do
  use ExUnit.Case
  alias QuickieSynth.Sound

  test "outputs the appropriate command to play a C note" do
    assert Sound.command("C") == "play -qn synth 2 pluck C"
  end
end

Alright, so this is just a basic test to define a function that knows how to output the appropriate command to create that sound. Run the test, just to make sure we haven't got a syntax error or anything silly.

Alright, so everything's good and we just need to define the module. We'll do that at the top of the test for now:

defmodule QuickieSynth.Sound do
  def command(note) do
    "play -qn synth 2 pluck #{note}"
  end
end

Run the tests, and they're green. Let's extract this module into the lib dir now.

Alright, so I know that this module was trivial, but there are a lot of different things you might want to do there - for instance, passing a struct defining the synth to use - and now there's a seam in the software for that logic to live in later, with no up front costs to complexity.

Next, we'll just add something to the README describing how to execute the command, and then we'll build the function we claimed existed. This is what I've seen referred to as README-Driven Development and I always enjoy it. Open up README.md and write it up:

# Quickie Synth

An Elixir-based synthesizer.  To use it, you can evaluate
`QuickieSynth.Sound.play("C")`.

Alright, so let's move to make the README not so much of a liar. Open back up lib/quickie_synth/sound.ex and add the play function, which will just pipe the output of the command/1 function to System.cmd/1.

  def play(note) do
    note |> command |> System.cmd
  end

Next, open up an iex session and let's see if it works. There's no reason it shouldn't:

iex -S mix
QuickieSynth.Sound.play("C")

Alright, so we have an elixir module that will play all the sounds we want. You can play with it a bit more:

QuickieSynth.Sound.play("E")
QuickieSynth.Sound.play("G")

Now before someone gets all upset, yes this clearly has potential for abuse with unsafe input. I don't much care about security for this demo. Don't pipe untrusted user input to this, ok? I shouldn't have to tell you this. :)

Composition

Now, before we get too much further, let's build a very trivial Composition module that can take a string that represents a series of notes, and a time to sleep between notes, and will spawn new processes for each note in the composition. I'm not really sure how to test this part, and I'm eager to get to the next part, so for now we'll just do more README-Driven Development.

Open up the README, and add a bit about Compositions:

If you want to build a composition, you can do that like so:

    QuickieSynth.Composition.play(100, "CEGCEGAAB")

Now, let's make this work. Open up lib/quickie_synth/composition.ex and let's get hacking:

defmodule QuickieSynth.Composition do
  # So what we want to do here is to split the string into a list of notes, and
  # then for each note we'll spawn a process to play it, sleep for the specified
  # timing, and then move on to the next note.
  def play(timing, notes) do
    for note <- String.graphemes(notes), do: spawn_note(timing, note)
  end

  # So now that we've described what we want to do, it's trivial
  defp spawn_note(timing, note) do
    spawn(QuickieSynth.Sound, :play, [note])
    :timer.sleep(timing)
  end
end

Let's try it out. Open up an iex -S mix and run the README's composition:

QuickieSynth.Composition.play(100, "CEGCEGAAB")

Awesome! That's pretty great, given the tiny amount of time we've spent on this so far. We're all of 10 lines of code in, and we're spawning a background process for each note - pretty neat. There's a little bit of an issue here - we can't play any sharps or flats with the Composition as it stands. If it took in a list of strings instead of splitting the string, it would be more flexible but the interface might be less pleasant. I'll leave tweaking that bit to you. We're going to move on to something else.

User Interface

So for the finale for this episode, I want to build a user interface for interacting with the synth. You'll just hit a key on your keyboard, and the appropriate note will play.

The first thing we'll have to do is map keys on the keyboard to the appropriate notes. I've whipped up a quick mapping from the bottom row of keys to the corresponding white keys on a piano keyboard, and the appropriate keys on the second row of the keyboard map to the appropriate sharps. Here's a quick test for it:

defmodule QuickieSynth.KeyboardMapTest do
  use ExUnit.Case

  test "bottom row" do
    assert_map("z", "G")
    assert_map("x", "A")
    assert_map("c", "B")
    assert_map("v", "C")
    assert_map("b", "D")
    assert_map("n", "E")
    assert_map("m", "F")
    assert_map(",", "G")
  end

  test "second row" do
    assert_map("s", "G#")
    assert_map("d", "A#")
    assert_map("g", "C#")
    assert_map("h", "D#")
    assert_map("k", "F#")
    assert_map("l", "G#")
  end

  test "unmapped keys" do
    assert_map("q", :nomap)
  end

  defp assert_map(key, note) do
    assert QuickieSynth.KeyboardMap.note_for(key) == note
  end
end

Alright, so here I've just mapped a picture of a piano keyboard to the bottom two rows of the computer keyboard. Let's run the test.

We're missing the KeyboardMap module, so let's implement it at the top of the test file:

# Here I'll just copy our assertions in, write a quick vim macro, and define
# functions for each mapping
defmodule QuickieSynth.KeyboardMap do
  def note_for("z"), do: "G"
  def note_for("x"), do: "A"
  def note_for("c"), do: "B"
  def note_for("v"), do: "C"
  def note_for("b"), do: "D"
  def note_for("n"), do: "E"
  def note_for("m"), do: "F"
  def note_for(","), do: "G"
  def note_for("s"), do: "G#"
  def note_for("d"), do: "A#"
  def note_for("g"), do: "C#"
  def note_for("h"), do: "D#"
  def note_for("k"), do: "F#"
  def note_for("l"), do: "G#"
  def note_for(_),   do: :nomap
end

Run the tests, and they pass. Fantastic. Before we go on, We'll extract this to lib...

Alright, now for the actual UI. We'll do README-Driven Development, so open up README.md and add the following:

To run the keyboard ui, evaluate the following:

    QuickieSynth.UI.start

Then click in the window and play with the bottom two rows of your keyboard.

Alright, now to implement this.

Now what we'd like to do is sit in a loop, awaiting user input, and any time a key is pressed fire off a process that sounds the appropriate note. The easiest way to do this in Erlang is to use a deprecated module, gs. We'll use that for now, because it works :)

gs stands for Graphics System, and it's been superceded by wx, but it's very trivial to get keyboard events out of it. We'll build a module that spawns a new window and listens for keypress events from the window. Any time it receives a keypress, it spawns a new sound process.

Open up lib/quickie_synth/ui.ex:

defmodule QuickieSynth.UI do
  alias QuickieSynth.Sound
  alias QuickieSynth.KeyboardMap

  # [1] Alright, so there's a window that will capture our keyboard events.  We'll
  # create a window, then run a loop to receive events from the window's
  # process.
  def start do
    window = create_window(200, 100)
    loop(window)
  end

  # [3] Finally, we'll grab the key that was pressed, map it to a note, and
  # spawn the sound if it was a mapped note
  def loop(window) do
    receive do
      {:gs, window, :keypress, data, args} ->
        key = hd(args)
        note = KeyboardMap.note_for("#{key}")
        case note do
          :nomap -> :ok
          _ -> spawn(Sound, :play, [note])
        end
      _ -> :ok
    end
    loop(window)
  end

  # [2] OK, so creating a window with gs is pretty easy.  We're going to start the
  # gs_server, then create a new window that we care about keypress events from
  defp create_window(width, height) do
    gs_server = :gs.start()
    :gs.create(:window, gs_server, [width: width, height: height, keypress: true, map: true])
  end
end

Alright, so spin up an iex session iex -S mix and launch the UI with QuickieSynth.UI.start. Now click in the window and play with the bottom row of the keyboard.

Summary

In today's episode, we very rapidly developed a synthesizer module and a user interface for it, using the wonders of shelling out to the underlying system and spawning off a lot of tiny Elixir processes. I had a lot of fun with this one, and I hope you did as well. See you soon!

Resources