CHIP-8 in Common Lisp: Sound

Posted on December 26th, 2016.

In the previous posts we looked at how to emulate a CHIP-8 CPU with Common Lisp, added a screen to see the results, and added user input so we could play games. This is good enough for basic play, but if we want the full experience we'll need to add sound. Let's finish the emulator.

The full series of posts so far:

  1. CHIP-8 in Common Lisp: The CPU
  2. CHIP-8 in Common Lisp: Graphics
  3. CHIP-8 in Common Lisp: Input
  4. CHIP-8 in Common Lisp: Sound
  5. CHIP-8 in Common Lisp: Disassembly
  6. CHIP-8 in Common Lisp: Debugging Infrastructure
  7. CHIP-8 in Common Lisp: Menus

The full emulator source is on BitBucket and GitHub.

  1. CHIP-8 Sound
  2. The Emulation Layer
    1. Data
    2. Instructions
    3. Timers
  3. Sound From Scratch
    1. Sine Waves
    2. Square Waves
    3. Sawtooth Waves
    4. Triangle Waves
  4. Playing Sound
    1. Sampling
    2. Buffering
    3. Configuration
    4. Angle Rate & Frequency
    5. Running the Sound Loop
    6. Threading Issues
  5. Result
  6. Future

CHIP-8 Sound

The CHIP-8 has an extremely simple sound and timer system. See Cowgod's documentation for an overview.

In a nutshell there are two registers: the "sound timer" and "delay timer". Each of these is decremented sixty times per second whenever they are non-zero.

The delay timer has no special behavior beyond this, but ROMs can read its value and use it as a real-time clock.

The sound timer cannot be read by ROMs, only written. Whenever its value is positive the CHIP-8's buzzer will sound. That's the extent of the CHIP-8's sound: one buzzer that's either on or off.

The Emulation Layer

Let's add the required registers and instructions to the emulator.

Data

First we'll add the registers into the chip struct:

(defstruct chip
  ; ...
  (delay-timer 0 :type fixnum)
  (sound-timer 0 :type fixnum)
  ; ...
  )

These are of type fixnum instead of int8 for reasons we'll see later.

Instructions

The CHIP-8 has three LD instructions for dealing with these registers. We actually saw them back in the first post, but now we know their purpose:

(macro-map                                              ;; LD
    (NAME           ARGLIST         DESTINATION   SOURCE)
    (; ...
     (op-ld-reg<dt  (_ r _ _)       (register r)  delay-timer)
     (op-ld-dt<reg  (_ r _ _)       delay-timer   (register r))
     (op-ld-st<reg  (_ r _ _)       sound-timer   (register r)))
  `(define-instruction ,name ,arglist
    (setf ,destination ,source)))

Timers

Next we'll need to decrement the timers at a rate of 60hz. We could do some math to figure out the number of cycles between each and do this in the main loop, but let's use a separate thread instead:

(defun run (rom-filename)
  (let ((chip (make-chip)))
    (setf *c* chip)
    (load-rom chip rom-filename)
    (bt:make-thread (curry #'run-cpu chip))
    (bt:make-thread (curry #'run-timers chip)) ; NEW
    (chip8.gui.screen::run-gui chip)))

Now we just need to define the run-timers function that will be run in that separate thread:

(defun run-timers (chip)
  (iterate
    (while running)
    (decrement-timers chip)
    (sleep 1/60)))

Simple enough. Technically this will run slightly slower than 60hz, because it will take some time to actually decrement the timers, and our thread might not get woken up exactly 1/60 of a second later.

We could try to fix this by sleeping for less or using a timer wheel, but I think this is good enough for our needs here. We already have to suffer through GC pauses anyway, so trying to get absolute precision is going to be more trouble that it's worth.

Now to decrement the timers. The CPU thread might be executing a write instruction as this thread is updating, so we'll use SBCL's atomic compare-and-swap functionality to avoid losing decrements:

(defun decrement-timers (chip)
  (flet ((decrement (i)
           (if (plusp i)
             (1- i)
             0)))
    (with-chip (chip)
      (sb-ext:atomic-update delay-timer #'decrement)
      (sb-ext:atomic-update sound-timer #'decrement)))
  nil)

This is why we declared the timer slots in the chip struct to be fixnum — the SBCL manual states that for (atomic-update place function ...):

place can be any place supported by sb-ext:compare-and-swap

And that:

Built-in CAS-able places are accessor forms whose car is one of the following:

...

or the name of a defstruct created accessor for a slot whose declared type is either fixnum or t. Results are unspecified if the slot has a declared type other than fixnum or t.

(emphasis mine). Remember that delay-timer is macroleted to (chip-delay-timer ...) by with-chip, so it does refer to a "defstruct created accessor".

We could have used a lock from bordeaux threads to manage this portably (though more slowly), but I wanted to play around with SBCL's concurrency stuff.

Sound From Scratch

Now that we've got the timers all set up, all that's left is to play a sound whenever sound-timer is positive. We could do this in a number of ways, for example: loading a WAV file and looping it. But that's boring and almost cheating, so let's do it from scratch.

Sound is a pretty complicated beast. For this CHIP-8 emulator we'll only dip our toes into the water and work with the very basics. I'll explain some basics here but will simplify and gloss over a lot — there are plenty of resources online if you want to learn more.

For our purposes we'll think of "sound" as a pressure value over time. For example, a simple sound wave might look something like this:

Graph of a basic sound wave

The pressure starts at 0, gradually climbs until it hits 1, then falls and gradually hits -1, then returns to 0 and starts the process over again.

The distance from the highest pressure value to the lowest is called the "amplitude" of the wave (specifically "peak-to-peak amplitude"). For sound, this is what determines how loud the sound is. For the CHIP-8 emulator we'll always just be using pressure values between -1 and 1.

There are (infinitely) many different types of sound waves, each with their own distinct character. Let's look at a few of them that might fit well with the CHIP-8's retro aesthetic. This page has some example audio files so you can hear what they sound like.

Sine Waves

The wave I used as the example above is a sine wave. It's based on the sine function you might have learned about in trigonometry class.

We usually think of sin as taking an angle as an argument, not a time. But we can convert time to an appropriate angle value later, so let's not get hung up on that. Sine performs one complete "wave" in exactly τ radians:

Graph of a basic sine wave

Common Lisp has this built in as the sin function, so we don't have any work to do for this one.

Square Waves

The next wave we'll look at is the square wave. Instead of gradually moving between -1 and 1 over time like sine, it stays at 1 for half its wave then immediately jumps straight to -1:

Graph of a basic square wave

This "jump" gives the square wave kind of a "buzzy" character that you may have heard before if you've played many old computer games (or like to listen to chiptunes).

Common Lisp doesn't have this function built in, but we can make it pretty easily. First we'll define some useful constants:

(defconstant +pi+ (coerce pi 'single-float))
(defconstant +tau+ (* 2 +pi+))
(defconstant +1/4tau+ (* 1/4 +tau+))
(defconstant +1/2tau+ (* 1/2 +tau+))
(defconstant +3/4tau+ (* 3/4 +tau+))

We're using single-floats because our audio library is going to want those later.

Now we can define the square-wave function:

(defun sqr (angle)
  (if (< (mod angle +tau+) +1/2tau+)
    1.0
    -1.0))

I've called it sqr because my utility library already has a function called "square", and I like that it's three letters like the sin function.

The implementation is pretty simple. We just have to make sure to mod the angle by τ to make the results repeat properly, like this:

Graph of several square waves

Sawtooth Waves

The sawtooth wave is next up in our little menagerie of waveforms. The name comes from what it looks like when you have a few in a row:

Graph of several sawtooth waves

A single wave of it looks like this:

Graph of a basic sawtooth wave

Sawtooth waves still have a bit of a "buzzy" feel to them because of the jump halfway through their period, but unlike square waves they have some gradual change, so they're often a nice happy medium.

To implement the saw function, notice that for the first half of the wave's life (0 to τ) it goes from 0 to 1, and for the second half (½τ to τ) it goes from -1 to 0. We'll also remember to mod by τ to proper repeating:

(defun saw (angle)
  (let ((a (mod angle +tau+)))
    (if (< a +1/2tau+)
      (map-range 0   +1/2tau+
                 0.0 1.0
                 a)
      (map-range +1/2tau+ +tau+
                 -1.0     0.0
                 a))))

map-range is a really handy function from my utility library. I wish I could think of a better name for it. It's kind of like a lerp function, but instead of assuming the input value is between 0 and 1 it allows you to specify the input range.

map-range takes five arguments:

(map-range source-start source-end
           dest-start   dest-end
           value)

I think of it as taking a source number line with a value on it, stretching that number line to become the destination line, and finding the new location of the value:

         2  3  4  5  6                2  3  4  5  6
         ━━◉━━━━━━━━━━                ━━━━━━━━━◎━━━
        ╱  │          ╲              ╱         │   ╲
       ╱   │           ╲            ╱          │    ╲
      ╱ ┌──┘            ╲          ╱           └───┐ ╲
     ╱  │                ╲        ╱                │  ╲
    ╱   ▼                 ╲      ╱                 ▼   ╲
    ━━━━◉━━━━━━━━━━━━━━━━━━━     ━━━━━━━━━━━━━━━━━━◎━━━━━
    0  1  2  3  4  5  6  7 8     0  1  2  3  4  5  6  7 8

Triangle Waves

Let's look at one more kind of wave before moving on: the triangle wave. As you might expect, this wave looks like a big triangle:

Graph of a basic triangle wave

Triangle waves are closer to sine waves than square or sawtooth waves were, but they've still got a bit of "sharpness" to them because they don't have that gradual rounding off at the peak like sine waves.

Much like saw, we can define tri by defining each half of the wave separately:

(defun tri (angle)
  (let ((a (mod angle +tau+)))
    (if (< a +1/2tau+)
      (map-range 0    +1/2tau+
                 -1.0 1.0
                 a)
      (map-range +1/2tau+ +tau+
                 1.0      -1.0
                 a))))

That's it for our whirlwind tour of simple sound waves.

Playing Sound

We've got four functions (sin, sqr, saw, and tri) that take in angles and spit out pressure values between -1 and 1, so the next step is to somehow use them to make the computer play sound.

We'll be using PortAudio and cl-portaudio to handle the OS and sound device interaction. If you're following along you'll need to install PortAudio separately (before Quickloading cl-portaudio). On OS X you can do this with brew install portaudio, for Linux use your distro's package manager.

Sampling

In the real world pressure and time vary continuously, but our computer handles audio differently. Modern digital audio uses the concept of sampling to split up the pressure-over-time graph into discrete pieces. Instead of trying to work with an infinite number of times, we look at a finite number of individual samples.

A "sample" is just a pressure value at a particular point in time. The number of times per second the computer reads or writes a sample is called the "sample rate". If the sampling rate is too low, we won't be able to tell much about the original wave, and playing it would sound like noise:

Graph of a sparse sampling

But a higher sample rate can get us nice and close to the original wave:

Graph of a dense sampling

We'll stick with the most common sample rate, 44.1khz, because it's the most widely supported (even though it's overkill for our simple waves):

(defconstant +sample-rate+ 44100d0)

Buffering

It would be wasteful to keep constantly calling back and forth between our code and PortAudio's code, so instead we'll be giving PortAudio a buffer of samples to play which will effectively contain a "chunk" of sound. This buffer will be a vanilla Lisp array of single-floats. The size of the buffer is configurable, with larger buffers representing bigger chunks of sound.

There's a tradeoff between efficiency and responsiveness we need to decide on. Larger buffers are more efficient (less switching back and forth between us and PortAudio) but once we've sent a buffer it's going to play to its end — we can't stop it midway. I've found 512 to be a happy medium:

(defconstant +audio-buffer-size+ 512
  "The number of samples in the audio buffer.")

(defconstant +audio-buffer-time+ (* +audio-buffer-size+ (/ +sample-rate+))
  "The total time the information in the audio buffer represents, in seconds.")

(defun make-audio-buffer ()
  (make-array +audio-buffer-size+
    :element-type 'single-float
    :initial-element 0.0))

A 512-sample buffer with a sample rate of 44100 samples per second means that each buffer will represent about 11.6 milliseconds of sound.

We're going to need a way to fill an audio buffer with sample values, so let's make a fill-buffer function:

(defun fill-buffer (buffer function rate start)
  (iterate
    (for i :index-of-vector buffer)
    (for angle :from start :by rate)
    (setf (aref buffer i) (funcall function angle))
    (finally (return (mod angle +tau+)))))

fill-buffer will take one of our four waveform functions and fill the given buffer with samples generated by it. rate will be the rate at which the angle should be incremented per-sample, which we'll figure out in a moment.

The one snag is that we can't just start from an angle of zero each time we fill a buffer (unless our buffer size happens to exactly match the period of our wave). If we did we'd only ever be sending the first chunk of our wave, and we'd end up with something like:

Graph of a shitty buffer filling strategy

This is obviously not what we want. The solution is to return the angle we ended at from fill-buffer, and then pass it in as start on the next round so we can pick up where we left off.

Configuration

Since we've gone to the trouble of writing four separate wave functions, each with their own character/timbre, let's make the sound the emulator plays configurable. First we'll make some helper functions for filling the audio buffer with a particular wave:

(defun fill-square (buffer rate start)
  (fill-buffer buffer #'sqr rate start))

(defun fill-sine (buffer rate start)
  (fill-buffer buffer #'sin rate start))

(defun fill-sawtooth (buffer rate start)
  (fill-buffer buffer #'saw rate start))

(defun fill-triangle (buffer rate start)
  (fill-buffer buffer #'tri rate start))

Then we'll add a slot to the chip struct:

(defstruct chip
  ; ...
  (sound-type :sine :type keyword)
  ; ...
  )

And we'll make a helper function for retrieving the appropriate buffer-filling function:

(defun audio-buffer-filler (chip)
  (ecase (chip-sound-type chip)
    (:square #'fill-square)
    (:sine #'fill-sine)
    (:sawtooth #'fill-sawtooth)
    (:triangle #'fill-triangle)))

We'll use ecase instead of vanilla case so we get a nicer error message if the slot is set to something incorrect. I actually find myself reaching for ecase more often than case these days because a silent nil result is almost never what I want.

Angle Rate & Frequency

One last bit we need to figure out is how much to increment the angle for each sample.

All four of our wave functions assume that one wave happens in exactly τ radians. So if we assume that we want one wave per second, and there are +sample-rate+ samples in every second, we'd just use (/ +tau+ +sample-rate+) to get the angle increment.

But one wave per second is below the range of human hearing. We want something more like 440 waves per second (the "frequency" of the note A440), so our function to find the angle increment will looks like this:

(defun audio-rate (frequency)
  (coerce (* (/ +tau+ +sample-rate+) frequency) 'single-float))

We coerce it to a single-float here to avoid having to do it on every addition later.

Running the Sound Loop

We've now got all the bits and pieces we need to make some noise. Let's build a run-sound function bit by bit. First we initialize the output stream with PortAudio:

(defun run-sound (chip)
  (portaudio:with-audio
    (portaudio:with-default-audio-stream
        (audio-stream 0 1
                      :sample-format :float
                      :sample-rate +sample-rate+
                      :frames-per-buffer +audio-buffer-size+)
      ; ...
      ))
  nil)

The 0 1 arguments mean "zero input channels" (we don't need access to the microphone!) and "one output channel".

Now we'll add our sound loop:

(defun run-sound (chip)
  (portaudio:with-audio
    (portaudio:with-default-audio-stream
        (audio-stream 0 1
                      :sample-format :float
                      :sample-rate +sample-rate+
                      :frames-per-buffer +audio-buffer-size+)
      (with-chip (chip)                              ; NEW
        (iterate (with buffer = (make-audio-buffer)) ; NEW
                 (with angle = 0.0)                  ; NEW
                 (with rate = (audio-rate 440))      ; NEW
                 (while running)                     ; NEW
                 (if (plusp sound-timer)             ; NEW
                   ; ...                             ; NEW
                   (sleep +audio-buffer-time+))))))  ; NEW
  nil)

We create a buffer and some extra variables, then we check if the sound timer is positive. If not, we just sleep for a while. I decided to sleep for the same amount of time as a single audio buffer would take so that each iteration of the loop represents roughly the same slice of time, but this isn't strictly necessary.

If the sound timer is positive we'll need to fill the buffer with pressure values and ship it off to PortAudio:

(defun run-sound (chip)
  (portaudio:with-audio
    (portaudio:with-default-audio-stream
        (audio-stream 0 1
                      :sample-format :float
                      :sample-rate +sample-rate+
                      :frames-per-buffer +audio-buffer-size+)
      (with-chip (chip)
        (iterate (with buffer = (make-audio-buffer))
                 (with angle = 0.0)
                 (with rate = (audio-rate 440))
                 (while running)
                 (if (plusp sound-timer)
                   (progn                                            ; NEW
                     (setf angle (funcall (audio-buffer-filler chip) ; NEW
                                          buffer rate angle))        ; NEW
                     (portaudio:write-stream audio-stream buffer))   ; NEW
                   (sleep +audio-buffer-time+))))))
  nil)

Pretty simple: just fill the buffer and ship it. We keep track of the angle the buffer-filler returned so we can pass it in on the next iteration to avoid the "truncated waves" problem we talked about earlier.

Threading Issues

In a perfect world we could just add one more thread like we did with the others and be done with it:

(defun run (rom-filename)
  (let ((chip (make-chip)))
    (setf *c* chip)
    (load-rom chip rom-filename)
    (bt:make-thread (curry #'run-cpu chip))
    (bt:make-thread (curry #'run-timers chip))
    (bt:make-thread (curry #'run-sound chip))  ; NEW
    (chip8.gui.screen::run-gui chip)))

Unfortunately there's one more snag we need to deal with. It turns out that Qt becomes very unhappy if we set up our threads this way. I'm not 100% sure what the problem is, but it has something to do with control of the main thread on OS X.

The solution is to let Qt take control of the main thread, and spawn all our other threads from there. We'll update run-gui to take a thunk and call it once it's ready:

                     ; NEW
(defun run-gui (chip thunk)
  (with-main-window
      (window (make-screen chip))
    (funcall thunk)))            ; NEW

And now we can move all the thread spawning into the thunk:

(defun run (rom-filename &key start-paused)
  (let ((chip (make-chip)))
    (setf *c* chip)
    (load-rom chip rom-filename)
    (chip8.gui.screen::run-gui chip
      (lambda ()                                       ; NEW
        (bt:make-thread (curry #'run-cpu chip))        ; NEW
        (bt:make-thread (curry #'run-timers chip))     ; NEW
        (bt:make-thread (curry #'run-sound chip))))))  ; NEW

Technically only the sound thread needs to be handled this way, but I figured I'd treat them all the same.

Result

That's it! Now you can play games and should get a nice loud buzz when the sound should fire. ufo.rom is a good game to test it out with — it should buzz whenever you fire and whenever a ship gets hit. Turn down your speakers if you don't want to scare the cat.

The sound type can be changed on the fly (e.g. (setf (chip-sound-type *c*) :sawtooth)), so play around and decide which type is your favorite. I'm partial to sawtooth myself.

Future

With that we've got a full-featured CHIP-8 emulator! It works, but there's still work left to be done. In the next few posts we'll look at things like:

Thanks to Joe Karl for reading a draft of this post.