ReapNES

NES Music Reverse Engineering Studio — Extract, validate, and reproduce NES game music at frame-level fidelity

View on GitHub

NSF to MIDI Pipeline

What It Does

Converts any NES NSF file into playable REAPER projects with full musical scoring — notes, volume envelopes, duty cycle changes, and drum patterns — using the same ReapNES_APU.jsfx synth plugin that the Castlevania pipeline produces.

How It Works

Stage 1: 6502 Emulation (nsf_player.py / nsf_to_reaper.py)

The NSF file contains the actual NES sound driver code extracted from the game ROM. We run it on a py65 6502 CPU emulator:

1. Load NSF data at the load address ($9000 for Mega Man)
2. Call INIT(song_number) — the driver sets up the song
3. Call PLAY() 60 times per second — the driver processes one frame
4. After each PLAY call, capture all APU register values ($4000-$4017)
5. Record: which frame, which register, what value

This produces a frame-level register dump — identical to what a Mesen APU trace captures, but without needing to run the game.

Stage 2: Register-to-MIDI Conversion

The APU registers map directly to musical parameters:

$4000 bits 7-6: Pulse 1 duty cycle    → CC12 (timbre)
$4000 bits 3-0: Pulse 1 volume        → CC11 (expression)
$4002-$4003:    Pulse 1 period         → MIDI note number
$4004-$4007:    Same for Pulse 2
$4008:          Triangle linear counter → note duration
$400A-$400B:    Triangle period         → MIDI note number
$400C:          Noise volume            → drum velocity
$400E:          Noise period + mode     → drum type (hi-hat/snare/kick)

Each frame’s register state becomes MIDI events:

Stage 3: MIDI File Assembly

The MIDI file has 5 tracks matching the CV1 format:

Track 0: Metadata
  - Tempo, time signature
  - Game name, song name, source info
  - Loop markers

Track 1: Square 1 [lead]
  - Channel 0, Program 80
  - Notes + CC11 (volume envelope) + CC12 (duty cycle)

Track 2: Square 2 [harmony]
  - Channel 1, Program 81
  - Notes + CC11 + CC12

Track 3: Triangle [bass]
  - Channel 2, Program 38
  - Notes + CC11

Track 4: Noise [drums]
  - Channel 3
  - GM drum mapping (36=kick, 38=snare, 42=hi-hat)

Stage 4: REAPER Project Generation (generate_project.py)

The existing project generator creates a proper .rpp file:

- RPP v7 format with GUIDs
- 4 named tracks with color coding
- FXCHAIN per track with ReapNES_APU.jsfx loaded
- Slider parameters: duty cycle, volume, enable, channel mode
- MIDI item referencing the generated .mid file
- Correct tempo and time signature

The Command

# One song:
python scripts/nsf_to_reaper.py game.nsf 3 90 -o output/Game/

# All songs:
python scripts/nsf_to_reaper.py game.nsf --all -o output/Game/ \
    --names "Stage Select,Cut Man,Guts Man,..."

# Then regenerate proper REAPER projects:
for midi in output/Game/midi/*.mid; do
    python scripts/generate_project.py --midi "$midi" --nes-native \
        -o "output/Game/reaper/$(basename ${midi%.mid}.rpp)"
done

What You Get

For each song:

Open the .rpp in REAPER → hit play → hear NES-accurate audio with full per-frame envelope detail. Edit notes in piano roll. Change duty cycles. Remix.

Validated On

The NSF pipeline produces output structurally identical to the ROM parser pipeline. Same MIDI format, same REAPER project format, same synth plugin. The only difference is the source: ROM parser reads game data directly, NSF emulator runs the driver code.