NESynth - NES Step Sequencer Synthesizer (6502 Assembly)
A step sequencer synthesizer for the NES, written entirely in 6502 assembly. 4 channels, 16-step patterns, 4 segments per channel, a piano roll editor, and multiple sound presets.
Download the ROM: nesynth.nes — runs on any NES emulator or flash cart.
The source code can be found on GitHub.
This was another fun AI exploration while I was exploring how to make music for the NES for Ninja Turdle II.
Demo
How It Works
The NES has a built-in Audio Processing Unit (APU) with 5 sound channels. NESynth uses 4 of them:
| Channel | Type | Description |
|---|---|---|
| C1 | Pulse 1 | Square wave with configurable duty cycle (12.5%, 25%, 50%, 75%) |
| C2 | Pulse 2 | Second pulse oscillator, same capabilities as C1 |
| C3 | Triangle | Fixed triangle waveform — clean bass and sustained tones |
| C4 | Noise | Pseudo-random noise generator — drums and percussion |
Each channel has its own 16-step grid, sound preset, envelope mode, tempo, and segment selection. Notes are placed on an E minor pentatonic scale spanning 4 octaves (20 pitch levels: E, G, A, B, D).
The sequencer runs in a loop driven by the NMI (VBLANK) interrupt. Every frame, the code advances the step counter, reads the sequence data for each channel, and writes directly to the APU registers to produce sound. I haven't overly stress-tested it, but it seems to support pretty fast sounds.
Sound Presets
Each channel type has its own set of sound presets that configure the APU registers differently:
Pulse Channels (C1/C2) — 8 presets:
| Preset | Character |
|---|---|
| LED | Warm lead (50% duty, full volume) |
| STB | Bright stab (12.5% duty) |
| BAS | Fat bass (75% duty) |
| PAD | Chaos pad (75% duty, low volume) |
| BUZ | Thin buzz (12.5% duty, mid volume) |
| SQR | Square medium (50% duty) |
| FUL | Full power (75% duty, max volume) |
| TNK | Bright tonk (25% duty) |
Triangle Channel (C3) - 4 presets: NRM (normal), STC (staccato), SHT (short), PLS (ultra-short pulse)
Noise Channel (C4) — 6 presets: KCK (kick), SNR (snare), HTC (closed hi-hat), HTO (open hi-hat), TOM (tom), RIM (rimshot)
Excuse my names for some of the sound settings, I was a bit of a loss for how to appropriately name things and took some creative liberties. :)
Envelope Modes
The pulse channels support 4 envelope modes that shape how notes decay over time:
| Mode | Behavior |
|---|---|
| SUS | Sustain — constant volume for the full step |
| PLK | Pluck — fast volume decay (~0.5s), like a plucked string |
| TRM | Tremolo — volume oscillation (~1.3s cycle) |
| SLW | Slow fade — long gradual decay (~2.4s) |
These are implemented by writing new volume values to the APU on every frame.
Controls
PLAY Mode (default)
| Button | Action |
|---|---|
| Select | Cycle active channel (C1 → C2 → C3 → C4) |
| Up/Down | Cycle parameter (SND, WAV, SEQ, SEG, SPD, LCK) |
| Left/Right | Change parameter value |
| Start | Enter EDIT mode |
EDIT Mode
| Button | Action |
|---|---|
| Left/Right | Move cursor across the 16-step grid |
| Up/Down | Change pitch at cursor |
| A (tap) | Place or remove note |
| A + Left/Right | Extend note with hold/tie markers for sustain |
| B (tap) | Clear step at cursor |
| B + Left/Right | Erase sweep — clears steps as you move |
| Select | Cycle channel |
| Start | Return to PLAY mode |
Segments
Each channel has 4 independent 16-step segments. With SEQ set to ALL, the sequencer auto-advances through all 4 segments,skipping empty segments, giving you 64 steps of total sequence length per channel. Set SEQ to SEG to loop a single segment, or OFF to silence the channel.
Architecture
The entire program is a single 6502 assembly file assembled with ca65 (from the cc65 suite) and linked with ld65. It targets the simplest NES mapper — NROM (mapper 0) — with 16KB of PRG ROM and 8KB of CHR ROM. Total ROM size is 24KB.
Sequence data lives in 256 bytes of RAM at $0300, organized as 4 channels x 4 segments x 16 steps. Each step stores a pitch index (1-20 for notes, 0 for empty, $FE/$FF for hold/tie markers).
The display uses phased nametable updates split across 2 frames to stay within the NES's tight VBLANK budget. Frame 1 updates the step indicators and grids for channels 1-2. Frame 2 updates channels 3-4 and the parameter display. This keeps all PPU writes fast enough to avoid visual glitches.
Building
Requires cc65 (ca65 assembler + ld65 linker).
make
Produces nesynth.nes.
Arrived
Ninja Turdle