596 lines
No EOL
46 KiB
HTML
596 lines
No EOL
46 KiB
HTML
<!DOCTYPE html>
|
||
<html lang='en'><head><meta charset='utf-8' /><meta name='pinterest' content='nopin' /><link href='http://stevelosh.com/static/css/style.css' rel='stylesheet' type='text/css' /><link href='http://stevelosh.com/static/css/print.css' rel='stylesheet' type='text/css' media='print' /><title>CHIP-8 in Common Lisp: Sound / Steve Losh</title></head><body><header><a id='logo' href='http://stevelosh.com/'>Steve Losh</a><nav><a href='http://stevelosh.com/blog/'>Blog</a> - <a href='http://stevelosh.com/projects/'>Projects</a> - <a href='http://stevelosh.com/photography/'>Photography</a> - <a href='http://stevelosh.com/links/'>Links</a> - <a href='http://stevelosh.com/rss.xml'>Feed</a></nav></header><hr class='main-separator' /><main id='page-blog-entry'><article><h1><a href='index.html'>CHIP-8 in Common Lisp: Sound</a></h1><p class='date'>Posted on December 26th, 2016.</p><p>In the previous posts we looked at how to emulate a <a href="https://en.wikipedia.org/wiki/CHIP-8">CHIP-8</a> 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 <a href="http://makegames.tumblr.com/post/1136623767/finishing-a-game">finish</a> the emulator.</p>
|
||
|
||
<p>The full series of posts so far:</p>
|
||
|
||
<ol>
|
||
<li><a href="../chip8-cpu/index.html">CHIP-8 in Common Lisp: The CPU</a></li>
|
||
<li><a href="../chip8-graphics/index.html">CHIP-8 in Common Lisp: Graphics</a></li>
|
||
<li><a href="../chip8-input/index.html">CHIP-8 in Common Lisp: Input</a></li>
|
||
<li><a href="index.html">CHIP-8 in Common Lisp: Sound</a></li>
|
||
<li><a href="../../../2017/01/chip8-disassembly/index.html">CHIP-8 in Common Lisp: Disassembly</a></li>
|
||
<li><a href="../../../2017/01/chip8-debugging-infrastructure/index.html">CHIP-8 in Common Lisp: Debugging Infrastructure</a></li>
|
||
<li><a href="../../../2017/01/chip8-menus/index.html">CHIP-8 in Common Lisp: Menus</a></li>
|
||
</ol>
|
||
|
||
<p>The full emulator source is on <a href="https://bitbucket.org/sjl/cl-chip8">BitBucket</a> and <a href="https://github.com/sjl/cl-chip8">GitHub</a>.</p>
|
||
|
||
<ol class="table-of-contents"><li><a href="index.html#s1-chip-8-sound">CHIP-8 Sound</a></li><li><a href="index.html#s2-the-emulation-layer">The Emulation Layer</a><ol><li><a href="index.html#s3-data">Data</a></li><li><a href="index.html#s4-instructions">Instructions</a></li><li><a href="index.html#s5-timers">Timers</a></li></ol></li><li><a href="index.html#s6-sound-from-scratch">Sound From Scratch</a><ol><li><a href="index.html#s7-sine-waves">Sine Waves</a></li><li><a href="index.html#s8-square-waves">Square Waves</a></li><li><a href="index.html#s9-sawtooth-waves">Sawtooth Waves</a></li><li><a href="index.html#s10-triangle-waves">Triangle Waves</a></li></ol></li><li><a href="index.html#s11-playing-sound">Playing Sound</a><ol><li><a href="index.html#s12-sampling">Sampling</a></li><li><a href="index.html#s13-buffering">Buffering</a></li><li><a href="index.html#s14-configuration">Configuration</a></li><li><a href="index.html#s15-angle-rate-frequency">Angle Rate & Frequency</a></li><li><a href="index.html#s16-running-the-sound-loop">Running the Sound Loop</a></li><li><a href="index.html#s17-threading-issues">Threading Issues</a></li></ol></li><li><a href="index.html#s18-result">Result</a></li><li><a href="index.html#s19-future">Future</a></li></ol>
|
||
|
||
<h2 id="s1-chip-8-sound"><a href="index.html#s1-chip-8-sound">CHIP-8 Sound</a></h2>
|
||
|
||
<p>The CHIP-8 has an <em>extremely</em> simple sound and timer system. See <a href="http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.5">Cowgod's
|
||
documentation</a> for an overview.</p>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>The delay timer has no special behavior beyond this, but ROMs can read its value
|
||
and use it as a real-time clock.</p>
|
||
|
||
<p>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.</p>
|
||
|
||
<h2 id="s2-the-emulation-layer"><a href="index.html#s2-the-emulation-layer">The Emulation Layer</a></h2>
|
||
|
||
<p>Let's add the required registers and instructions to the emulator.</p>
|
||
|
||
<h3 id="s3-data"><a href="index.html#s3-data">Data</a></h3>
|
||
|
||
<p>First we'll add the registers into the <code>chip</code> struct:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defstruct</span></i> chip
|
||
<span class="comment">; ...
|
||
</span> <span class="paren2">(<span class="code">delay-timer 0 <span class="keyword">:type</span> fixnum</span>)</span>
|
||
<span class="paren2">(<span class="code">sound-timer 0 <span class="keyword">:type</span> fixnum</span>)</span>
|
||
<span class="comment">; ...
|
||
</span> </span>)</span></span></code></pre>
|
||
|
||
<p>These are of type <code>fixnum</code> instead of <code>int8</code> for reasons we'll see later.</p>
|
||
|
||
<h3 id="s4-instructions"><a href="index.html#s4-instructions">Instructions</a></h3>
|
||
|
||
<p>The CHIP-8 has three <code>LD</code> instructions for dealing with these registers. We
|
||
actually saw them back in the first post, but now we know their purpose:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code">macro-map <span class="comment">;; LD
|
||
</span> <span class="paren2">(<span class="code">NAME ARGLIST DESTINATION SOURCE</span>)</span>
|
||
<span class="paren2">(<span class="code"><span class="comment">; ...
|
||
</span> <span class="paren3">(<span class="code">op-ld-reg<dt <span class="paren4">(<span class="code">_ r _ _</span>)</span> <span class="paren4">(<span class="code">register r</span>)</span> delay-timer</span>)</span>
|
||
<span class="paren3">(<span class="code">op-ld-dt<reg <span class="paren4">(<span class="code">_ r _ _</span>)</span> delay-timer <span class="paren4">(<span class="code">register r</span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">op-ld-st<reg <span class="paren4">(<span class="code">_ r _ _</span>)</span> sound-timer <span class="paren4">(<span class="code">register r</span>)</span></span>)</span></span>)</span>
|
||
`<span class="paren2">(<span class="code"><i><span class="symbol">define-instruction</span></i> ,name ,arglist
|
||
<span class="paren3">(<span class="code">setf ,destination ,source</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<h3 id="s5-timers"><a href="index.html#s5-timers">Timers</a></h3>
|
||
|
||
<p>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:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run <span class="paren2">(<span class="code">rom-filename</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">chip <span class="paren5">(<span class="code">make-chip</span>)</span></span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">setf <span class="special">*c*</span> chip</span>)</span>
|
||
<span class="paren3">(<span class="code">load-rom chip rom-filename</span>)</span>
|
||
<span class="paren3">(<span class="code">bt:make-thread <span class="paren4">(<span class="code">curry #'run-cpu chip</span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">bt:make-thread <span class="paren4">(<span class="code">curry #'run-timers chip</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren3">(<span class="code">chip8.gui.screen::run-gui chip</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>Now we just need to define the <code>run-timers</code> function that will be run in that
|
||
separate thread:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run-timers <span class="paren2">(<span class="code">chip</span>)</span>
|
||
<span class="paren2">(<span class="code">iterate
|
||
<span class="paren3">(<span class="code">while running</span>)</span>
|
||
<span class="paren3">(<span class="code">decrement-timers chip</span>)</span>
|
||
<span class="paren3">(<span class="code">sleep 1/60</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>Simple enough. Technically this will run slightly <em>slower</em> 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.</p>
|
||
|
||
<p>We could try to fix this by sleeping for less or using a <a href="https://github.com/npatrick04/timer-wheel">timer wheel</a>, 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.</p>
|
||
|
||
<p>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:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> decrement-timers <span class="paren2">(<span class="code">chip</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">flet</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">decrement <span class="paren5">(<span class="code">i</span>)</span>
|
||
<span class="paren5">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren6">(<span class="code">plusp i</span>)</span>
|
||
<span class="paren6">(<span class="code">1- i</span>)</span>
|
||
0</span>)</span></span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren4">(<span class="code">chip</span>)</span>
|
||
<span class="paren4">(<span class="code">sb-ext:atomic-update delay-timer #'decrement</span>)</span>
|
||
<span class="paren4">(<span class="code">sb-ext:atomic-update sound-timer #'decrement</span>)</span></span>)</span></span>)</span>
|
||
nil</span>)</span></span></code></pre>
|
||
|
||
<p>This is why we declared the timer slots in the <code>chip</code> struct to be <code>fixnum</code>
|
||
— the <a href="http://www.sbcl.org/manual/#Atomic-Operations">SBCL manual</a> states that for <code>(atomic-update place function ...)</code>:</p>
|
||
|
||
<blockquote>
|
||
<p>place can be any place supported by <code>sb-ext:compare-and-swap</code></p>
|
||
</blockquote>
|
||
|
||
<p>And that:</p>
|
||
|
||
<blockquote>
|
||
<p>Built-in CAS-able places are accessor forms whose car is one of the following:</p>
|
||
|
||
<p>...</p>
|
||
|
||
<p><strong>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.</strong></p>
|
||
</blockquote>
|
||
|
||
<p>(emphasis mine). Remember that <code>delay-timer</code> is <code>macrolet</code>ed to
|
||
<code>(chip-delay-timer ...)</code> by <code>with-chip</code>, so it does refer to a "defstruct
|
||
created accessor".</p>
|
||
|
||
<p>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.</p>
|
||
|
||
<h2 id="s6-sound-from-scratch"><a href="index.html#s6-sound-from-scratch">Sound From Scratch</a></h2>
|
||
|
||
<p>Now that we've got the timers all set up, all that's left is to play a sound
|
||
whenever <code>sound-timer</code> is positive. We could do this in a number of ways, for
|
||
example: loading a <code>WAV</code> file and looping it. But that's boring and almost
|
||
cheating, so let's do it from scratch.</p>
|
||
|
||
<p><a href="https://en.wikipedia.org/wiki/Sound">Sound</a> 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.</p>
|
||
|
||
<p>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:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-basic.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-basic.png" alt="Graph of a basic sound wave"></a></p>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>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. <a href="http://public.wsu.edu/~jkrug/MUS364/audio/Waveforms.htm">This page</a> has some example audio files
|
||
so you can hear what they sound like.</p>
|
||
|
||
<h3 id="s7-sine-waves"><a href="index.html#s7-sine-waves">Sine Waves</a></h3>
|
||
|
||
<p>The wave I used as the example above is a <a href="https://en.wikipedia.org/wiki/Sine_wave">sine wave</a>. It's based on <a href="https://en.wikipedia.org/wiki/Sine">the
|
||
<code>sine</code> function</a> you might have learned about in trigonometry class.</p>
|
||
|
||
<p>We usually think of <code>sin</code> 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 <a href="https://www.youtube.com/watch?v=jG7vhMMXagQ">τ</a> radians:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-sine.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-sine.png" alt="Graph of a basic sine wave"></a></p>
|
||
|
||
<p>Common Lisp has this built in as the <code>sin</code> function, so we don't have any work
|
||
to do for this one.</p>
|
||
|
||
<h3 id="s8-square-waves"><a href="index.html#s8-square-waves">Square Waves</a></h3>
|
||
|
||
<p>The next wave we'll look at is the <a href="https://en.wikipedia.org/wiki/Square_wave">square wave</a>. 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:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-square.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-square.png" alt="Graph of a basic square wave"></a></p>
|
||
|
||
<p>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).</p>
|
||
|
||
<p>Common Lisp doesn't have this function built in, but we can make it pretty
|
||
easily. First we'll define some useful constants:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +pi+ <span class="paren2">(<span class="code">coerce pi 'single-float</span>)</span></span>)</span>
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +tau+ <span class="paren2">(<span class="code">* 2 +pi+</span>)</span></span>)</span>
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +1/4tau+ <span class="paren2">(<span class="code">* 1/4 +tau+</span>)</span></span>)</span>
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +1/2tau+ <span class="paren2">(<span class="code">* 1/2 +tau+</span>)</span></span>)</span>
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +3/4tau+ <span class="paren2">(<span class="code">* 3/4 +tau+</span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>We're using single-floats because our audio library is going to want those
|
||
later.</p>
|
||
|
||
<p>Now we can define the square-wave function:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> sqr <span class="paren2">(<span class="code">angle</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">< <span class="paren4">(<span class="code">mod angle +tau+</span>)</span> +1/2tau+</span>)</span>
|
||
1.0
|
||
-1.0</span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>I've called it <code>sqr</code> because my utility library already has a function called
|
||
"square", and I like that it's three letters like the <code>sin</code> function.</p>
|
||
|
||
<p>The implementation is pretty simple. We just have to make sure to <code>mod</code> the
|
||
angle by τ to make the results repeat properly, like this:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-square-repeat.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-square-repeat.png" alt="Graph of several square waves"></a></p>
|
||
|
||
<h3 id="s9-sawtooth-waves"><a href="index.html#s9-sawtooth-waves">Sawtooth Waves</a></h3>
|
||
|
||
<p>The <a href="https://en.wikipedia.org/wiki/Sawtooth_wave">sawtooth wave</a> 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:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-saw-repeat.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-saw-repeat.png" alt="Graph of several sawtooth waves"></a></p>
|
||
|
||
<p>A single wave of it looks like this:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-saw.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-saw.png" alt="Graph of a basic sawtooth wave"></a></p>
|
||
|
||
<p>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 <em>some</em> gradual
|
||
change, so they're often a nice happy medium.</p>
|
||
|
||
<p>To implement the <code>saw</code> 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 <code>mod</code> by τ to proper repeating:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> saw <span class="paren2">(<span class="code">angle</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">a <span class="paren5">(<span class="code">mod angle +tau+</span>)</span></span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren4">(<span class="code">< a +1/2tau+</span>)</span>
|
||
<span class="paren4">(<span class="code">map-range 0 +1/2tau+
|
||
0.0 1.0
|
||
a</span>)</span>
|
||
<span class="paren4">(<span class="code">map-range +1/2tau+ +tau+
|
||
-1.0 0.0
|
||
a</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p><a href="https://github.com/sjl/cl-losh/blob/master/DOCUMENTATION.markdown#map-range-function"><code>map-range</code></a> is a <em>really</em> handy function from my utility library.
|
||
I wish I could think of a better name for it. It's kind of like a <a href="https://en.wikipedia.org/wiki/Linear_interpolation">lerp</a>
|
||
function, but instead of assuming the input value is between 0 and 1 it allows
|
||
you to specify the input range.</p>
|
||
|
||
<p><code>map-range</code> takes five arguments:</p>
|
||
|
||
<pre><code>(map-range source-start source-end
|
||
dest-start dest-end
|
||
value)
|
||
</code></pre>
|
||
|
||
<p>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:</p>
|
||
|
||
<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>
|
||
|
||
<h3 id="s10-triangle-waves"><a href="index.html#s10-triangle-waves">Triangle Waves</a></h3>
|
||
|
||
<p>Let's look at one more kind of wave before moving on: the <a href="https://en.wikipedia.org/wiki/Triangle_wave">triangle wave</a>. As
|
||
you might expect, this wave looks like a big triangle:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-tri.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-tri.png" alt="Graph of a basic triangle wave"></a></p>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>Much like <code>saw</code>, we can define <code>tri</code> by defining each half of the wave
|
||
separately:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> tri <span class="paren2">(<span class="code">angle</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">a <span class="paren5">(<span class="code">mod angle +tau+</span>)</span></span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren4">(<span class="code">< a +1/2tau+</span>)</span>
|
||
<span class="paren4">(<span class="code">map-range 0 +1/2tau+
|
||
-1.0 1.0
|
||
a</span>)</span>
|
||
<span class="paren4">(<span class="code">map-range +1/2tau+ +tau+
|
||
1.0 -1.0
|
||
a</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>That's it for our whirlwind tour of simple sound waves.</p>
|
||
|
||
<h2 id="s11-playing-sound"><a href="index.html#s11-playing-sound">Playing Sound</a></h2>
|
||
|
||
<p>We've got four functions (<code>sin</code>, <code>sqr</code>, <code>saw</code>, and <code>tri</code>) 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.</p>
|
||
|
||
<p>We'll be using <a href="http://www.portaudio.com/">PortAudio</a> and <a href="https://filonenko-mikhail.github.io/cl-portaudio/">cl-portaudio</a> to handle the OS and sound
|
||
device interaction. If you're following along you'll need to install PortAudio
|
||
separately (before Quickloading <code>cl-portaudio</code>). On OS X you can do this with
|
||
<code>brew install portaudio</code>, for Linux use your distro's package manager.</p>
|
||
|
||
<h3 id="s12-sampling"><a href="index.html#s12-sampling">Sampling</a></h3>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>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 "<a href="http://wiki.audacityteam.org/wiki/Sample_Rates">sample
|
||
rate</a>". 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:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-sample-sparse.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-sample-sparse.png" alt="Graph of a sparse sampling"></a></p>
|
||
|
||
<p>But a higher sample rate can get us nice and close to the original wave:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-sample-dense.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-sample-dense.png" alt="Graph of a dense sampling"></a></p>
|
||
|
||
<p>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):</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +sample-rate+ 44100d0</span>)</span></span></code></pre>
|
||
|
||
<h3 id="s13-buffering"><a href="index.html#s13-buffering">Buffering</a></h3>
|
||
|
||
<p>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 <code>single-float</code>s. The size of the buffer is
|
||
configurable, with larger buffers representing bigger chunks of sound.</p>
|
||
|
||
<p>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 <code>512</code> to be a happy medium:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +audio-buffer-size+ 512
|
||
<span class="string">"The number of samples in the audio buffer."</span></span>)</span>
|
||
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +audio-buffer-time+ <span class="paren2">(<span class="code">* +audio-buffer-size+ <span class="paren3">(<span class="code">/ +sample-rate+</span>)</span></span>)</span>
|
||
<span class="string">"The total time the information in the audio buffer represents, in seconds."</span></span>)</span>
|
||
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> make-audio-buffer <span class="paren2">(<span class="code"></span>)</span>
|
||
<span class="paren2">(<span class="code">make-array +audio-buffer-size+
|
||
<span class="keyword">:element-type</span> 'single-float
|
||
<span class="keyword">:initial-element</span> 0.0</span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>We're going to need a way to fill an audio buffer with sample values, so let's
|
||
make a <code>fill-buffer</code> function:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> fill-buffer <span class="paren2">(<span class="code">buffer <i><span class="symbol">function</span></i> rate start</span>)</span>
|
||
<span class="paren2">(<span class="code">iterate
|
||
<span class="paren3">(<span class="code">for i <span class="keyword">:index-of-vector</span> buffer</span>)</span>
|
||
<span class="paren3">(<span class="code">for angle <span class="keyword">:from</span> start <span class="keyword">:by</span> rate</span>)</span>
|
||
<span class="paren3">(<span class="code">setf <span class="paren4">(<span class="code">aref buffer i</span>)</span> <span class="paren4">(<span class="code">funcall <i><span class="symbol">function</span></i> angle</span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">finally <span class="paren4">(<span class="code">return <span class="paren5">(<span class="code">mod angle +tau+</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p><code>fill-buffer</code> will take one of our four waveform functions and fill the given
|
||
buffer with samples generated by it. <code>rate</code> will be the rate at which the angle
|
||
should be incremented per-sample, which we'll figure out in a moment.</p>
|
||
|
||
<p>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 <em>did</em> we'd only ever be sending the first chunk of our wave, and
|
||
we'd end up with something like:</p>
|
||
|
||
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-borked.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-sound-borked.png" alt="Graph of a shitty buffer filling strategy"></a></p>
|
||
|
||
<p>This is obviously not what we want. The solution is to return the angle we
|
||
ended at from <code>fill-buffer</code>, and then pass it in as <code>start</code> on the next round so
|
||
we can pick up where we left off.</p>
|
||
|
||
<h3 id="s14-configuration"><a href="index.html#s14-configuration">Configuration</a></h3>
|
||
|
||
<p>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:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> fill-square <span class="paren2">(<span class="code">buffer rate start</span>)</span>
|
||
<span class="paren2">(<span class="code">fill-buffer buffer #'sqr rate start</span>)</span></span>)</span>
|
||
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> fill-sine <span class="paren2">(<span class="code">buffer rate start</span>)</span>
|
||
<span class="paren2">(<span class="code">fill-buffer buffer #'sin rate start</span>)</span></span>)</span>
|
||
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> fill-sawtooth <span class="paren2">(<span class="code">buffer rate start</span>)</span>
|
||
<span class="paren2">(<span class="code">fill-buffer buffer #'saw rate start</span>)</span></span>)</span>
|
||
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> fill-triangle <span class="paren2">(<span class="code">buffer rate start</span>)</span>
|
||
<span class="paren2">(<span class="code">fill-buffer buffer #'tri rate start</span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>Then we'll add a slot to the <code>chip</code> struct:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defstruct</span></i> chip
|
||
<span class="comment">; ...
|
||
</span> <span class="paren2">(<span class="code">sound-type <span class="keyword">:sine</span> <span class="keyword">:type</span> keyword</span>)</span>
|
||
<span class="comment">; ...
|
||
</span> </span>)</span></span></code></pre>
|
||
|
||
<p>And we'll make a helper function for retrieving the appropriate buffer-filling
|
||
function:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> audio-buffer-filler <span class="paren2">(<span class="code">chip</span>)</span>
|
||
<span class="paren2">(<span class="code">ecase <span class="paren3">(<span class="code">chip-sound-type chip</span>)</span>
|
||
<span class="paren3">(<span class="code"><span class="keyword">:square</span> #'fill-square</span>)</span>
|
||
<span class="paren3">(<span class="code"><span class="keyword">:sine</span> #'fill-sine</span>)</span>
|
||
<span class="paren3">(<span class="code"><span class="keyword">:sawtooth</span> #'fill-sawtooth</span>)</span>
|
||
<span class="paren3">(<span class="code"><span class="keyword">:triangle</span> #'fill-triangle</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>We'll use <code>ecase</code> instead of vanilla <code>case</code> so we get a nicer error message if
|
||
the slot is set to something incorrect. I actually find myself reaching for
|
||
<code>ecase</code> more often than <code>case</code> these days because a silent <code>nil</code> result is
|
||
almost never what I want.</p>
|
||
|
||
<h3 id="s15-angle-rate-frequency"><a href="index.html#s15-angle-rate-frequency">Angle Rate & Frequency</a></h3>
|
||
|
||
<p>One last bit we need to figure out is how much to increment the angle for each
|
||
sample.</p>
|
||
|
||
<p>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
|
||
<code>+sample-rate+</code> samples in every second, we'd just use <code>(/ +tau+ +sample-rate+)</code>
|
||
to get the angle increment.</p>
|
||
|
||
<p>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 <a href="https://en.wikipedia.org/wiki/A440_(pitch_standard)">A440</a>), so our
|
||
function to find the angle increment will looks like this:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> audio-rate <span class="paren2">(<span class="code">frequency</span>)</span>
|
||
<span class="paren2">(<span class="code">coerce <span class="paren3">(<span class="code">* <span class="paren4">(<span class="code">/ +tau+ +sample-rate+</span>)</span> frequency</span>)</span> 'single-float</span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>We coerce it to a <code>single-float</code> here to avoid having to do it on every addition
|
||
later.</p>
|
||
|
||
<h3 id="s16-running-the-sound-loop"><a href="index.html#s16-running-the-sound-loop">Running the Sound Loop</a></h3>
|
||
|
||
<p>We've now got all the bits and pieces we need to make some noise. Let's build
|
||
a <code>run-sound</code> function bit by bit. First we initialize the output stream with
|
||
PortAudio:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run-sound <span class="paren2">(<span class="code">chip</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">portaudio:with-audio</span></i>
|
||
<span class="paren3">(<span class="code"><i><span class="symbol">portaudio:with-default-audio-stream</span></i>
|
||
<span class="paren4">(<span class="code">audio-stream 0 1
|
||
<span class="keyword">:sample-format</span> <span class="keyword">:float</span>
|
||
<span class="keyword">:sample-rate</span> +sample-rate+
|
||
<span class="keyword">:frames-per-buffer</span> +audio-buffer-size+</span>)</span>
|
||
<span class="comment">; ...
|
||
</span> </span>)</span></span>)</span>
|
||
nil</span>)</span></span></code></pre>
|
||
|
||
<p>The <code>0 1</code> arguments mean "zero input channels" (we don't need access to the
|
||
microphone!) and "one output channel".</p>
|
||
|
||
<p>Now we'll add our sound loop:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run-sound <span class="paren2">(<span class="code">chip</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">portaudio:with-audio</span></i>
|
||
<span class="paren3">(<span class="code"><i><span class="symbol">portaudio:with-default-audio-stream</span></i>
|
||
<span class="paren4">(<span class="code">audio-stream 0 1
|
||
<span class="keyword">:sample-format</span> <span class="keyword">:float</span>
|
||
<span class="keyword">:sample-rate</span> +sample-rate+
|
||
<span class="keyword">:frames-per-buffer</span> +audio-buffer-size+</span>)</span>
|
||
<span class="paren4">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren5">(<span class="code">chip</span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren5">(<span class="code">iterate <span class="paren6">(<span class="code">with buffer = <span class="paren1">(<span class="code">make-audio-buffer</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren6">(<span class="code">with angle = 0.0</span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren6">(<span class="code">with rate = <span class="paren1">(<span class="code">audio-rate 440</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren6">(<span class="code">while running</span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren6">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren1">(<span class="code">plusp sound-timer</span>)</span> <span class="comment">; NEW
|
||
</span> <span class="comment">; ... ; NEW
|
||
</span> <span class="paren1">(<span class="code">sleep +audio-buffer-time+</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> nil</span>)</span></span></code></pre>
|
||
|
||
<p>We create a buffer and some extra variables, then we check if the sound timer is
|
||
positive. If <em>not</em>, 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.</p>
|
||
|
||
<p>If the sound timer <em>is</em> positive we'll need to fill the buffer with pressure
|
||
values and ship it off to PortAudio:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run-sound <span class="paren2">(<span class="code">chip</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">portaudio:with-audio</span></i>
|
||
<span class="paren3">(<span class="code"><i><span class="symbol">portaudio:with-default-audio-stream</span></i>
|
||
<span class="paren4">(<span class="code">audio-stream 0 1
|
||
<span class="keyword">:sample-format</span> <span class="keyword">:float</span>
|
||
<span class="keyword">:sample-rate</span> +sample-rate+
|
||
<span class="keyword">:frames-per-buffer</span> +audio-buffer-size+</span>)</span>
|
||
<span class="paren4">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren5">(<span class="code">chip</span>)</span>
|
||
<span class="paren5">(<span class="code">iterate <span class="paren6">(<span class="code">with buffer = <span class="paren1">(<span class="code">make-audio-buffer</span>)</span></span>)</span>
|
||
<span class="paren6">(<span class="code">with angle = 0.0</span>)</span>
|
||
<span class="paren6">(<span class="code">with rate = <span class="paren1">(<span class="code">audio-rate 440</span>)</span></span>)</span>
|
||
<span class="paren6">(<span class="code">while running</span>)</span>
|
||
<span class="paren6">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren1">(<span class="code">plusp sound-timer</span>)</span>
|
||
<span class="paren1">(<span class="code"><i><span class="symbol">progn</span></i> <span class="comment">; NEW
|
||
</span> <span class="paren2">(<span class="code">setf angle <span class="paren3">(<span class="code">funcall <span class="paren4">(<span class="code">audio-buffer-filler chip</span>)</span> <span class="comment">; NEW
|
||
</span> buffer rate angle</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren2">(<span class="code">portaudio:write-stream audio-stream buffer</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren1">(<span class="code">sleep +audio-buffer-time+</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span>
|
||
nil</span>)</span></span></code></pre>
|
||
|
||
<p>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.</p>
|
||
|
||
<h3 id="s17-threading-issues"><a href="index.html#s17-threading-issues">Threading Issues</a></h3>
|
||
|
||
<p>In a perfect world we could just add one more thread like we did with the others
|
||
and be done with it:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run <span class="paren2">(<span class="code">rom-filename</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">chip <span class="paren5">(<span class="code">make-chip</span>)</span></span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">setf <span class="special">*c*</span> chip</span>)</span>
|
||
<span class="paren3">(<span class="code">load-rom chip rom-filename</span>)</span>
|
||
<span class="paren3">(<span class="code">bt:make-thread <span class="paren4">(<span class="code">curry #'run-cpu chip</span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">bt:make-thread <span class="paren4">(<span class="code">curry #'run-timers chip</span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">bt:make-thread <span class="paren4">(<span class="code">curry #'run-sound chip</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren3">(<span class="code">chip8.gui.screen::run-gui chip</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>The solution is to let Qt take control of the main thread, and spawn all our
|
||
other threads from there. We'll update <code>run-gui</code> to take a thunk and call it
|
||
once it's ready:</p>
|
||
|
||
<pre><code><span class="code"> <span class="comment">; NEW
|
||
</span><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run-gui <span class="paren2">(<span class="code">chip thunk</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">with-main-window</span></i>
|
||
<span class="paren3">(<span class="code">window <span class="paren4">(<span class="code">make-screen chip</span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">funcall thunk</span>)</span></span>)</span></span>)</span> <span class="comment">; NEW</span></span></code></pre>
|
||
|
||
<p>And now we can move all the thread spawning into the thunk:</p>
|
||
|
||
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run <span class="paren2">(<span class="code">rom-filename &key start-paused</span>)</span>
|
||
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">chip <span class="paren5">(<span class="code">make-chip</span>)</span></span>)</span></span>)</span>
|
||
<span class="paren3">(<span class="code">setf <span class="special">*c*</span> chip</span>)</span>
|
||
<span class="paren3">(<span class="code">load-rom chip rom-filename</span>)</span>
|
||
<span class="paren3">(<span class="code">chip8.gui.screen::run-gui chip
|
||
<span class="paren4">(<span class="code"><i><span class="symbol">lambda</span></i> <span class="paren5">(<span class="code"></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren5">(<span class="code">bt:make-thread <span class="paren6">(<span class="code">curry #'run-cpu chip</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren5">(<span class="code">bt:make-thread <span class="paren6">(<span class="code">curry #'run-timers chip</span>)</span></span>)</span> <span class="comment">; NEW
|
||
</span> <span class="paren5">(<span class="code">bt:make-thread <span class="paren6">(<span class="code">curry #'run-sound chip</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span> <span class="comment">; NEW</span></span></code></pre>
|
||
|
||
<p>Technically only the sound thread needs to be handled this way, but I figured
|
||
I'd treat them all the same.</p>
|
||
|
||
<h2 id="s18-result"><a href="index.html#s18-result">Result</a></h2>
|
||
|
||
<p>That's it! Now you can play games and should get a nice loud buzz when the
|
||
sound should fire. <code>ufo.rom</code> 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.</p>
|
||
|
||
<p>The sound type can be changed on the fly (e.g. <code>(setf (chip-sound-type *c*)
|
||
:sawtooth)</code>), so play around and decide which type is your favorite. I'm
|
||
partial to sawtooth myself.</p>
|
||
|
||
<h2 id="s19-future"><a href="index.html#s19-future">Future</a></h2>
|
||
|
||
<p>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:</p>
|
||
|
||
<ul>
|
||
<li>A menu system for runtime configuration</li>
|
||
<li>Disassembling/debugging infrastructure</li>
|
||
<li>A graphical debugger</li>
|
||
</ul>
|
||
|
||
<p><em>Thanks to <a href="https://twitter.com/joekarl">Joe Karl</a> for reading a draft of this
|
||
post.</em></p>
|
||
</article></main><hr class='main-separator' /><footer><nav><a href='https://github.com/sjl/'>GitHub</a> ・ <a href='https://twitter.com/stevelosh/'>Twitter</a> ・ <a href='https://instagram.com/thirtytwobirds/'>Instagram</a> ・ <a href='https://hg.stevelosh.com/.plan/'>.plan</a></nav></footer></body></html> |