[084] PCM Audio

Generating PCM Audio and making some noise

Subscribe now

PCM Audio [05.13.2016]

In today's episode, we're going to generate PCM audio and play it back through my machine's speakers. Let's get started.

Project

Start off a new project with mix new pcm_playground and cd into it.

In order to do this, we're going to use a sine wave oscillator that I've already built. We'll reconstruct it quickly. Open up lib/sine_wave.ex:

defmodule SineWave do
  defstruct amplitude: 1, frequency: 440

  def value_at(%__MODULE__{amplitude: a, frequency: f}, time) do
    angular_frequency = 2 * f * :math.pi
    a * :math.sin(angular_frequency * time)
  end
end

So with this, you can generate a SineWave Oscillator of a given frequency and amplitude, and you can then get the value of that oscillator at a given time using the value_at function. I've rushed through this to get to the meat of the project, which is the PCM itself.

Now, I've provided a link to the Wikipedia page on Pulse-code Modulation in the Resources section. I'll provide a general description of how it works:

(( reference the PCM.svg file ))

So here you can see a wave representing audio over time. At a particular interval, the wave is sampled and the value recorded. Let's start writing our PCM sampler module. Open up lib/pcm_sampler.ex:

defmodule PcmSampler do
  # We're going to sample 16,000 times per second.
  @sample_rate 16_000
  # There's only a single channel involved
  @channels 1
  # and we're going to use 16 bit signed integers to represent each sample.
  # They have a maximum size of 32,767.
  @max_amplitude 32_767
end

Alright, so there are the parameters for the PCM audio we'll be sampling. Next we just need to actually sample it.

  # We'll define a function that knows how to sample a given oscillator for a
  # particular duration.
  def sample(oscillator=%SineWave{}, duration) do
    # We need to figure out how many samples the duration consists of.  Truncate
    # it so it's an integer.
    num_samples = trunc(@sample_rate * duration)
    # We'll gather the appropriate number of samples into a list.
    pre_data = for sample_number <- 1..num_samples do
      # The sine wave goes from -1 to 1.  The value of the sample will be the
      # max amplitude times the oscillator's value, truncated
      value = @max_amplitude * SineWave.value_at(oscillator, sample_number/@sample_rate)
              |> :erlang.trunc
      # We need to store it as a 16 bit signed integer.  I'm also making it
      # big-endian, to draw attention to the fact that the endianness matters
      <<value :: [big, signed, integer, size(16)]>>
    end

    # So now we have a list of all the values that this sample consists of, and
    # we want to generate a single binary from it.  We'll use iodata_to_binary
    iodata_to_binary(pre_data)
  end

That's it. This code will generate the audio for a given sine wave and duration. Next, we'll need to actually play it. On linux, I'll use a tool called pacat for this. I'm not sure what an equivalent tool is for the mac, but essentially you want a tool that you can tell to expect 16khz, 1 channel, big endian pcm audio on stdin, and it will play the audio on the system's audio device. I'm using pacat because I use pulseaudio, but for what it's worth pulseaudio will run on a mac, so worst-case scenario you can always use pacat as well.

Anyway, next we'll want to build an example that will launch pacat, configured for our settings, and push this sample into its stdin. We'll write an example that plays an A note for a second.

mkdir examples
vim examples/play.exs

First, we'll launch pacat in a port, with the appropriate configuration:

port = Port.open({:spawn, "pacat -p --channels=1 --rate=16000 --format=s16be"}, [:binary]) # s16be means signed, 16-bit, big-endian

Now we'll just generate a second of audio and pipe it into stdin:

frequency = 400
duration = 1

IO.puts "playing #{frequency}Hz for #{duration} seconds"
w = %SineWave{frequency: frequency}
data = PcmSampler.sample(w, duration)
send(port, {self, {:command, data}})

Alright, now we can save this file and run it with mix run examples/play.exs.

So that's pretty good. Of course, to come this far to play a mere second of audio would be a travesty, so here's a little diddy:

diddy = "BAGABBBAAABDD"

notes = %{
  ?A => 440,
  ?B => 493.88,
  ?G => 392,
  ?D => 587.33
}

duration = 0.5

for <<note <- diddy>> do
  frequency = notes[note]
  IO.puts "playing #{frequency}Hz for #{duration} seconds"
  w = %SineWave{frequency: frequency}
  data = PcmSampler.sample(w, duration)
  send(port, {self, {:command, data}})
end

Summary

In today's episode, we explored Pulse-code Modulation and generated audio streams from a simulated sine wave. If you want to look at some more fun similar projects, I've linked to a couple that I'm playing with in the resources. See you soon!

Resources