35680677eb4f

Add sound post
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Fri, 23 Dec 2016 12:00:54 -0500
parents 219d8676e914
children f0833ca00887
branches/tags (none)
files content/blog/2016/12/chip8-cpu.markdown content/blog/2016/12/chip8-graphics.markdown content/blog/2016/12/chip8-input.markdown content/blog/2016/12/chip8-sound.markdown static/media/images/blog/2016/12/chip8-sound-basic.png static/media/images/blog/2016/12/chip8-sound-borked.png static/media/images/blog/2016/12/chip8-sound-sample-dense.png static/media/images/blog/2016/12/chip8-sound-sample-sparse.png static/media/images/blog/2016/12/chip8-sound-saw-repeat.png static/media/images/blog/2016/12/chip8-sound-saw.png static/media/images/blog/2016/12/chip8-sound-sine.png static/media/images/blog/2016/12/chip8-sound-square-repeat.png static/media/images/blog/2016/12/chip8-sound-square.png static/media/images/blog/2016/12/chip8-sound-tri.png

Changes

--- a/content/blog/2016/12/chip8-cpu.markdown	Fri Dec 23 10:55:08 2016 -0500
+++ b/content/blog/2016/12/chip8-cpu.markdown	Fri Dec 23 12:00:54 2016 -0500
@@ -30,6 +30,12 @@
 
 This first post will deal with emulating the CHIP-8's CPU.
 
+The full series of posts so far:
+
+1. [CHIP-8 in Common Lisp: The CPU](http://stevelosh.com/blog/2016/12/chip8-cpu/)
+2. [CHIP-8 in Common Lisp: Graphics](http://stevelosh.com/blog/2016/12/chip8-graphics/)
+3. [CHIP-8 in Common Lisp: Input](http://stevelosh.com/blog/2016/12/chip8-input/)
+
 The full emulator source is on [BitBucket][] and [GitHub][].
 
 [imran]: http://imrannazar.com/GameBoy-Emulation-in-JavaScript
--- a/content/blog/2016/12/chip8-graphics.markdown	Fri Dec 23 10:55:08 2016 -0500
+++ b/content/blog/2016/12/chip8-graphics.markdown	Fri Dec 23 12:00:54 2016 -0500
@@ -10,6 +10,12 @@
 Lisp.  But a CPU alone isn't much fun to play, so in this post we'll add
 a screen to the emulator with [Qt][].
 
+The full series of posts so far:
+
+1. [CHIP-8 in Common Lisp: The CPU](http://stevelosh.com/blog/2016/12/chip8-cpu/)
+2. [CHIP-8 in Common Lisp: Graphics](http://stevelosh.com/blog/2016/12/chip8-graphics/)
+3. [CHIP-8 in Common Lisp: Input](http://stevelosh.com/blog/2016/12/chip8-input/)
+
 The full emulator source is on [BitBucket][] and [GitHub][].
 
 [CHIP-8]: https://en.wikipedia.org/wiki/CHIP-8
--- a/content/blog/2016/12/chip8-input.markdown	Fri Dec 23 10:55:08 2016 -0500
+++ b/content/blog/2016/12/chip8-input.markdown	Fri Dec 23 12:00:54 2016 -0500
@@ -1,6 +1,6 @@
 +++
 title = "CHIP-8 in Common Lisp: Input"
-snip = "Adding user interaction."
+snip = "Let's add a keypad."
 date = 2016-12-23T16:00:00Z
 draft = false
 
@@ -11,6 +11,12 @@
 like `maze.rom`, but in this post we'll add user input so we can actually *play*
 games.
 
+The full series of posts so far:
+
+1. [CHIP-8 in Common Lisp: The CPU](http://stevelosh.com/blog/2016/12/chip8-cpu/)
+2. [CHIP-8 in Common Lisp: Graphics](http://stevelosh.com/blog/2016/12/chip8-graphics/)
+3. [CHIP-8 in Common Lisp: Input](http://stevelosh.com/blog/2016/12/chip8-input/)
+
 The full emulator source is on [BitBucket][] and [GitHub][].
 
 [CHIP-8]: https://en.wikipedia.org/wiki/CHIP-8
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content/blog/2016/12/chip8-sound.markdown	Fri Dec 23 12:00:54 2016 -0500
@@ -0,0 +1,665 @@
++++
+title = "CHIP-8 in Common Lisp: Sound"
+snip = "Let's add a buzzer."
+date = 2016-12-30T14:50:00Z
+draft = true
+
++++
+
+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](http://stevelosh.com/blog/2016/12/chip8-cpu/)
+2. [CHIP-8 in Common Lisp: Graphics](http://stevelosh.com/blog/2016/12/chip8-graphics/)
+3. [CHIP-8 in Common Lisp: Input](http://stevelosh.com/blog/2016/12/chip8-input/)
+
+The full emulator source is on [BitBucket][] and [GitHub][].
+
+[CHIP-8]: https://en.wikipedia.org/wiki/CHIP-8
+[BitBucket]: https://bitbucket.org/sjl/cl-chip8
+[GitHub]: https://github.com/sjl/cl-chip8
+[finish]: http://makegames.tumblr.com/post/1136623767/finishing-a-game
+
+<div id="toc"></div>
+
+## CHIP-8 Sound
+
+The CHIP-8 has an *extremely* simple sound and timer system.  See [Cowgod's
+documentation][cg-sound] 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.
+
+[cg-sound]: http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.5
+
+## 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:
+
+```lisp
+(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:
+
+```lisp
+(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:
+
+```lisp
+(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:
+
+```lisp
+(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.
+
+[timer wheel]: https://github.com/npatrick04/timer-wheel
+
+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:
+
+```lisp
+(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 `macrolet`ed 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.
+
+[SBCL manual]: http://www.sbcl.org/manual/#Atomic-Operations
+
+## 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
+
+[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.
+
+[Sound]: https://en.wikipedia.org/wiki/Sound
+
+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](/media/images/blog/2016/12/chip8-sound-basic.png)](/media/images/blog/2016/12/chip8-sound-basic.png)
+
+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][waveforms] has some example audio files
+so you can hear what they sound like.
+
+[waveforms]: http://public.wsu.edu/~jkrug/MUS364/audio/Waveforms.htm
+
+### Sine Waves
+
+The wave I used as the example above is a [sine wave][].  It's based on [the
+`sine` function][sine] 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 [τ][tau] radians:
+
+[![Graph of a basic sine wave](/media/images/blog/2016/12/chip8-sound-sine.png)](/media/images/blog/2016/12/chip8-sound-sine.png)
+
+Common Lisp has this built in as the `sin` function, so we don't have any work
+to do for this one.
+
+[sine]: https://en.wikipedia.org/wiki/Sine
+[sine wave]: https://en.wikipedia.org/wiki/Sine_wave
+[tau]: https://www.youtube.com/watch?v=jG7vhMMXagQ
+
+### 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](/media/images/blog/2016/12/chip8-sound-square.png)](/media/images/blog/2016/12/chip8-sound-square.png)
+
+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:
+
+```lisp
+(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:
+
+```lisp
+(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](/media/images/blog/2016/12/chip8-sound-square-repeat.png)](/media/images/blog/2016/12/chip8-sound-square-repeat.png)
+
+[square wave]: https://en.wikipedia.org/wiki/Square_wave
+
+### 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](/media/images/blog/2016/12/chip8-sound-saw-repeat.png)](/media/images/blog/2016/12/chip8-sound-saw-repeat.png)
+
+A single wave of it looks like this:
+
+[![Graph of a basic sawtooth wave](/media/images/blog/2016/12/chip8-sound-saw.png)](/media/images/blog/2016/12/chip8-sound-saw.png)
+
+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:
+
+```lisp
+(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`][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:
+
+<pre class="lineart">
+         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
+</pre>
+
+[sawtooth wave]: https://en.wikipedia.org/wiki/Sawtooth_wave
+[map-range]: https://github.com/sjl/cl-losh/blob/master/DOCUMENTATION.markdown#map-range-function
+[lerp]: https://en.wikipedia.org/wiki/Linear_interpolation
+
+### 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](/media/images/blog/2016/12/chip8-sound-tri.png)](/media/images/blog/2016/12/chip8-sound-tri.png)
+
+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:
+
+```lisp
+(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.
+
+[triangle wave]: https://en.wikipedia.org/wiki/Triangle_wave
+
+## 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.
+
+[PortAudio]: http://www.portaudio.com/
+[cl-portaudio]: https://filonenko-mikhail.github.io/cl-portaudio/
+
+### 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](/media/images/blog/2016/12/chip8-sound-sample-sparse.png)](/media/images/blog/2016/12/chip8-sound-sample-sparse.png)
+
+But a higher sample rate can get us nice and close to the original wave:
+
+[![Graph of a dense sampling](/media/images/blog/2016/12/chip8-sound-sample-dense.png)](/media/images/blog/2016/12/chip8-sound-sample-dense.png)
+
+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):
+
+```lisp
+(defconstant +sample-rate+ 44100d0)
+```
+
+[sample rate]: http://wiki.audacityteam.org/wiki/Sample_Rates
+
+### 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-float`s.  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:
+
+```lisp
+(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've going to need a way to fill an audio buffer with sample values, so let's
+make a `fill-buffer` function:
+
+```lisp
+(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](/media/images/blog/2016/12/chip8-sound-borked.png)](/media/images/blog/2016/12/chip8-sound-borked.png)
+
+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:
+
+```lisp
+(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:
+
+```lisp
+(defstruct chip
+  ; ...
+  (sound-type :sine :type keyword)
+  ; ...
+  )
+```
+
+And we'll make a helper function for retrieving the appropriate buffer-filling
+function:
+
+```lisp
+(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:
+
+```lisp
+(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.
+
+[A440]: https://en.wikipedia.org/wiki/A440_(pitch_standard)
+
+### 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:
+
+```lisp
+(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:
+
+```lisp
+(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:
+
+```lisp
+(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:
+
+```lisp
+(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:
+
+```lisp
+                     ; 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:
+
+```lisp
+(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:
+
+* A menu system for runtime configuration
+* Disassembling/debugging infrastructure
+* A graphical debugger
+
Binary file static/media/images/blog/2016/12/chip8-sound-basic.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-borked.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-sample-dense.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-sample-sparse.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-saw-repeat.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-saw.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-sine.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-square-repeat.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-square.png has changed
Binary file static/media/images/blog/2016/12/chip8-sound-tri.png has changed