The NES sound chip

@xromdev/apu v0.1.0 · MIT

A TypeScript library that is the NES's audio chip — the 2A03's APU — register by register: writes in, samples out. Every game's music reaches the speaker as writes to these 24 registers, so this plays any of them. Below, the library runs live in your browser: every control is a bit of a real register.

npm install @xromdev/apunpm ↗GitHub ↗README ↗
channels, all of them
5
channels, all of them
gzipped, no dependencies
~6 KB
gzipped, no dependencies
faster than real time
~500×
faster than real time
tests, measured not eyeballed
71
tests, measured not eyeballed
Keys playoctave +0

The chip starts silent. Play any key with the mouse, or from your keyboard — A W S E D F T G Y H U J K O L P ; play the keys under the coloured bar, Z / X move it an octave. Each note writes the period and the high byte that starts it. Or pick a sound beside a channel.

Pulse 1

Level 0–15, ~16 ms

chip off

Pulse 2

Level 0–15, ~16 ms

chip off

Triangle

Level 0–15, ~16 ms

No volume at all — it sounds or it does not. An octave below a pulse at the same period.

chip off

Noise

Level 0–15, ~16 ms

chip off

DMC

Level 0–127, ~16 ms

1-bit samples read from cartridge memory: every bit steps a 7-bit output up or down by 2. The samples below are made on this page — or load your own, and it is encoded for the DMC in your browser.

1 bytes = 0.00 s at this rate

chip off

Output — all five channels through the non-linear mixer and the console's filters

Registers $4000–$4017

$4000
B8
P1 duty/vol
$4001
08
P1 sweep
$4002
FD
P1 lo
$4003
08
P1 hi/len
$4004
78
P2 duty/vol
$4005
08
P2 sweep
$4006
A9
P2 lo
$4007
09
P2 hi/len
$4008
FF
TRI linear
$4009
00
—
$400A
FD
TRI lo
$400B
08
TRI hi/len
$400C
36
NOI vol
$400D
00
—
$400E
06
NOI mode/rate
$400F
08
NOI len
$4010
0F
DMC rate/loop
$4011
00
DMC level
$4012
00
DMC address
$4013
00
DMC length
$4014
00
OAM DMA
$4015
0F
enable
$4016
00
joypad
$4017
00
frame ctr

Last writes

  1. none yet

Use it

The registers as they are now. Register writes in, a finished buffer out. What you last played here, as code — play it or save it as WAV, rendered by the library in your browser.

import { renderWrites } from '@xromdev/apu';

const { samples, sampleRate } = renderWrites(
  [
    { time: 0, address: 0x4015, value: 0x0F }, // enable: P1 P2 TRI NOI
    { time: 0, address: 0x4000, value: 0xB8 }, // duty 50%, volume 8, halt
    { time: 0, address: 0x4001, value: 0x08 }, // sweep off
    { time: 0, address: 0x4002, value: 0xFD },
    { time: 0, address: 0x4003, value: 0x08 }, // length 254 — starts the note
    { time: 0, address: 0x4004, value: 0x78 }, // duty 25%, volume 8, halt
    { time: 0, address: 0x4005, value: 0x08 }, // sweep off
    { time: 0, address: 0x4006, value: 0xA9 },
    { time: 0, address: 0x4007, value: 0x09 }, // length 254 — starts the note
    { time: 0, address: 0x4008, value: 0xFF }, // triangle: sounds until stopped
    { time: 0, address: 0x400A, value: 0xFD },
    { time: 0, address: 0x400B, value: 0x08 }, // length 254 — starts the note
    { time: 0, address: 0x400C, value: 0x36 },
    { time: 0, address: 0x400E, value: 0x06 }, // noise long mode, rate 6
    { time: 0, address: 0x400F, value: 0x08 }, // length 254 — starts the note
    { time: 0, address: 0x4010, value: 0x0F }, // DMC rate 15
    { time: 0, address: 0x4012, value: 0x00 }, // sample at $C000
    { time: 0, address: 0x4013, value: 0x00 }, // 1 bytes long
  ],
  2 // seconds
);