I wanted to know what the music in these games actually is. Not what a wiki says. So I built a tool that reads the cartridge and runs the game's own code, and I pointed it at all six. Every number on this page came out of that tool, and the command that printed it stands next to it. Where something is not proven, the sentence says so.
What a song here is
Open No Mercy and listen to the title theme. Nothing on that cartridge is a recording of it. What is on the cartridge is a list of instructions — play note 52 for 24 ticks, switch to instrument 7, pan hard left, drop the tempo — and a bank of short samples the instructions point at. A player routine reads the list sixty times a second and mixes the result. This is a tracker module in all but name, and it is why the music survives at cartridge sizes that could never hold audio.
The arithmetic is the whole argument. bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64 reports 29 songs and effect banks, 36,201 notes, 70,252 events. The 27 real songs are 145,782 bytes of sequence and run 39.6 minutes end to end. The same 39.6 minutes as a 128 kbit/s MP3 would be 38,016,000 bytes — 36.3 MiB on a 32 MiB cartridge, before a single texture or model.
A track is read byte by byte, and the top bit decides what a byte is. Under 0x80 it is a note; from 0x80 up it is a command. That is the whole grammar:
byte < 0x80 a note; 0x60 is the rest
[velocity] one byte, only while velocity mode is on
length 1 byte if < 0x80, else 15 bits over 2 bytes
byte >= 0x80 a command, 0x80..0xAC
0x81 wave n pick the instrument (the sample) for this channel
0x8B fixed_length use one length for every note that follows
0x90 envelope n load envelope n from the song's envelope table
0x95 loop_start c repeat the next stretch c times; 0xFF means forever
0x9C pan p p >> 1, so 0..127 over the stereo field
0xA6 volume v
0x80 stop end of track
# the codes and their names come from VPW Studio and are no evidence in
# themselves. What each one DOES was measured separately - see below.
sngWalkTrack in src/audio.js. It is not allowed to guess: a track counts as read only if it ends in stop or in an endless loop, with no byte left over. Across all six cartridges 138 of 142 songs and effect banks pass that bar — the tool prints the four that only reach likely.The songs are long and they repeat. 25 of No Mercy's 27 songs end in an endless loop rather than a stop, the longest running 486.1 s before it comes back round. The first two games do the opposite: 0 of 19 and 0 of 28 songs loop, because their player has no loop of that kind at all. More on that split below.
VADPCM: 16 samples in 9 bytes
Every sample in every one of these games is VADPCM — the N64's own compressed audio format, decoded by the RSP. If you write music, the useful way to think about it is as a very small, very fast predictive codec with no psychoacoustics in it at all. It does not throw away what you cannot hear. It throws away precision, and it guesses.
A frame is 16 samples in 9 bytes. The first byte carries a 4-bit shift and a 4-bit index into a codebook. The other 8 bytes carry 16 signed 4-bit residuals. To decode one sample the codec predicts it from the two samples before it, using the coefficient pair the index selected, then adds the residual scaled by the shift. That is it: a second-order predictor, one of four filters per frame — eight, in one World Tour bank — and four bits of correction per sample.
The codebook is stored with the sample, not with the format, so two samples in one bank can use different ones. --banks prints them: every sample in Revenge and No Mercy is book 2x4 — order 2, four predictors. World Tour's first bank is 191 samples of 2x4; its second mixes 70 samples of 2x8, eight predictors, with 2 of 2x4.
16-bit PCM 2 bytes per sample
VADPCM 9 bytes per 16 samples = 0.5625 bytes per sample
a fixed 3.556 : 1, every frame, no exceptions
at 28,800 Hz 28,800 * 0.5625 = 16,200 bytes per second
raw would be = 57,600 bytes per second
so 1 MiB = 64.7 s 1 MB = 61.7 s
30 s = 486,000 B 3 min = 2,916,000 B
The decoder in the tool is not trusted on its own word. Every sample that carries a loop also carries the 16 predictor samples the game saved at the loop point, so the hardware can jump back without a click. Decoding the sample and comparing the frame at that offset against those 16 stored values is a check the ROM itself grades. bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64 prints VADPCM-Loop-State: 66/66. Revenge 24/24, WrestleMania 2000 63/63, VPW2 27/27. The first two games store no loop points, so they score 0/0 and prove nothing — the tool says info there, not ok.
The rate, out of the game's own code
This is the part worth telling as a story, because it is the difference between reading a wiki and running the code.
Two games hand you the number
In World Tour and Virtual Pro-Wrestling 64 the rate is a constant sitting in the delay slot of the call that sets it. The tool finds osAiSetFrequency by shape — the function that touches the audio interface registers and clamps its argument at 132 — then looks for callers that load an immediate into the argument register. There is exactly one value: 22,050. Status confirmed, source osAiSetFrequency. Nothing clever needed.
The other four do not
Revenge, WrestleMania 2000, VPW2 and No Mercy use a different audio library, and there is no constant next to the call. The rate lives in a configuration structure that the game builds at start-up and hands to MusInitialize. You can find a plausible immediate near the call site by pattern — and the tool does, and marks that path likely, because a pattern is a guess.
So it does the other thing. It maps the audio interface registers into a fake address space, writes the emulated configuration into memory, sets the argument register to it, and runs MusInitialize in a MIPS interpreter until the game writes the hardware register itself. Then it reads what the game passed and what the game wrote. No pattern, no assumption — a data flow, watched.
$ bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64
output 28800 Hz (libmus, effectively 28805.8 Hz,
VI clock 48681812 confirmed, frame rate 60 Hz ntsc)
dac divider 1690 (confirmed, computed 1690): osAiSetFrequency
0x80030730 emulated with 28800 Hz: AI_DACRATE = 1689
(divider 1690), AI_BITRATE = 15, return value 28805 Hz
rate confirmed (MusInitialize runtime): MusInitialize 0x80026F60
run with the emulated configuration (352520 instructions):
the game calls osAiSetFrequency 0x80030730 itself with
28800 Hz and writes AI_DACRATE = 1689 (divider 1690)
Three probes stand behind that line, and each one could have failed on its own:
- The video clock. The N64 derives audio timing from the video clock, which depends on the TV standard.
probeViClockfinds every branch onosTvType, runs each one three times with type 0, 1 and 2, and accepts only a site that produces three different clocks in the plausible range. All six cartridges land on 48,681,812, status confirmed. - The divider.
probeDacRatecallsosAiSetFrequencyon its own, with the rate the game asked for, and reads the register it writes. No Mercy:AI_DACRATE = 1689, so the divider is 1690. The routine also returns the rate it really achieved: 28,805. - The rate itself.
probeRuntimeRateis theMusInitializerun above. It is the only one of the three that reads the game's intent rather than the hardware's behaviour.
Put them together and the number a musician would care about falls out: 48,681,812 ÷ 1690 = 28,805.8 Hz. The game asks for 28,800. The hardware produces 28,805.8. The gap is 0.02 %, about a third of a cent — inaudible, but it is the difference between a figure that was computed and a figure that was assumed, and this site cares about that difference. In the first two games the same arithmetic gives 48,681,812 ÷ 2208 = 22,047.9 Hz against a requested 22,050.
The sequence: 45 commands, 22 with a formula
The player dispatches commands through a table of function pointers. The tool finds it the same way it finds everything else: a run of 43 to 64 consecutive words that are all addresses inside the boot segment, most of which are function starts. No Mercy's table sits at 0x80052E74.
Having the table is not the same as knowing what the commands do. So each handler is called — in the interpreter, on a fake channel structure filled with known values, with an argument stream of known bytes. Two things are measured from that.
How many bytes each command eats
Run the handler with an argument stream of 0x01 bytes, then with 0x81 bytes, and watch the return value and the highest byte it read. That gives the argument length, including the ones that are variable. All 45 handlers in No Mercy, WrestleMania 2000 and VPW2 report the length the format predicts; Revenge has only 43 commands and reports 43 of 43. The tool prints this as Player commands: 45/45 argument lengths confirmed.
What each command changes
The harder question is which field of the channel a command writes, and with what formula. The probe runs each handler over five different argument sets, diffs the channel structure before and after, and tries to match every changed byte against the value the tool's own reader predicts. A command earns confirmed only when both the field and the formula line up on all five.
For No Mercy the verdict is 22 confirmed, 18 likely, 5 with no effect on the channel at all. The five are stereo, print, start_effect, change_effect and marker — they act outside the channel, so this probe has nothing to say about them and the tool says so rather than guessing. Revenge, with its smaller table, gives 22 / 17 / 4.
tempo ticks_per_frame = trunc(trunc(bpm * 24576 / 120) / 60)
confirmed by emulating the tempo handler with four
different tempos; in No Mercy it lands in channel
field 156, in Revenge in field 188
envelope a 7-byte entry: rate, start, attack, peak, decay,
sustain, release
rateDiv = trunc(1024 / rate)
confirmed with 3 different entries against the
envelope handler, same channel fields every time
curve level = base +/- trunc(slope * ((ticks * rateDiv) >> 10))
this is the renderer's formula. It was then FOUND in the
game code, in that form: one function, all three phases,
each writing its level into the same channel field.
One more check closes the loop between the two halves of the tool. The renderer has its own table of what each command does to its own channel object. probeRenderBindings feeds both the renderer and the effect table the same five argument sets and compares every field. All six cartridges report the same line: 39 commands, 75 field bindings, 0 mismatches, 0 unbound effects. The fortieth rendered command is start_effect, which changes nothing on the channel, so there is nothing to bind.
And a check on the data, not the code
Alongside the note track, each channel carries two side streams for volume and pitch, encoded as runs. How a run length maps to ticks is a one-byte question with an obvious wrong answer. The tool tests three candidate biases against every loop-free track in the game and asks which one makes the streams cover the track exactly. Revenge: 55 of 55 streams exact at +2, 0 at +1, 0 at +3. WrestleMania 2000 29 of 29, VPW2 34 of 34, No Mercy 18 of 18 with 2 that also fit +1. One candidate wins everywhere, and the two it beat are printed beside it.
What differs between the six
The obvious guess is that the three early games share one engine and the three late ones another. The measurement says otherwise. The split is two and four, and it falls in the middle of 1998.
World Tour and Virtual Pro-Wrestling 64 use the N64 SDK's own audio library — the tool labels it libultra — with cseq sequences and ALBankFile banks at 22,050 Hz. Revenge had already moved to the player the tool labels libmus: sng sequences, PtrTablesV2 banks, 28,800 Hz. That is the same engine WrestleMania 2000, VPW2 and No Mercy use two years later. Revenge's command table is one revision older, 43 entries instead of 45, and at this layer that is the only difference between it and No Mercy.
| World Tour | VPW64 | Revenge | WM2000 | VPW2 | No Mercy | |
|---|---|---|---|---|---|---|
| Product code | NWNE | NVPJ | NW2E | NWXE | NA2J | NW4E |
| Cartridge | 12 MB | 16 MB | 16 MB | 32 MB | 32 MB | 32 MB |
| Player | libultra | libultra | libmus | libmus | libmus | libmus |
| Output rate | 22,050 | 22,050 | 28,800 | 28,800 | 28,800 | 28,800 |
| Rate evidenced by | constant | constant | runtime | runtime | runtime | runtime |
| DAC divider | 2208 | 2208 | 1690 | 1690 | 1690 | 1690 |
| Rate the hardware makes | 22,047.9 | 22,047.9 | 28,805.8 | 28,805.8 | 28,805.8 | 28,805.8 |
| Sequence format | cseq | cseq | sng (43 cmd) | sng (45) | sng (45) | sng (45) |
| Bank format | ALBankFile | ALBankFile | PtrTablesV2 | PtrTablesV2 | PtrTablesV2 | PtrTablesV2 |
| Banks | 2 | 2 | 2 | 3 | 8 | 3 |
| Samples | 263 | 300 | 201 | 238 | 516 | 293 |
| Bytes of VADPCM | 2,307,018 | 2,481,370 | 2,939,913 | 9,228,150 | 12,504,447 | 9,054,513 |
| Loop states checked | 0/0 | 0/0 | 24/24 | 63/63 | 27/27 | 66/66 |
| Songs | 19 | 28 | 21 | 16 | 19 | 27 |
| Effect banks | 0 | 0 | 1 | 2 | 7 | 2 |
| Notes | 25,374 | 42,053 | 54,006 | 44,788 | 46,461 | 36,201 |
| Events | 27,450 | 44,735 | 122,242 | 99,195 | 104,967 | 70,252 |
| Channels per song | 2–8 | 2–8 | 7–19 | 10–18 | 7–19 | 4–18 |
| Longest song | 171.8 s | 173.5 s | 442.0 s | 389.3 s | 389.3 s | 486.1 s |
| Songs that loop forever | 0 of 19 | 0 of 28 | 19 of 21 | 15 of 16 | 18 of 19 | 25 of 27 |
| Bytes of sequence | 99,267 | 159,356 | 316,742 | 229,090 | 232,804 | 145,782 |
| Longest sample | 6.128 s | 6.128 s | 4.591 s | 19.412 s | 46.323 s | 23.307 s |
| Command meaning | — | — | 22/17/4 | 22/18/5 | 22/18/5 | 22/18/5 |
| Renderer vs effect table | 0 mismatches | 0 | 0 | 0 | 0 | 0 |
| Parse status | 19 conf. | 28 conf. | 22 conf. | 18 conf. | 23 conf., 3 likely | 28 conf., 1 likely |
| Bank assignment | 19 likely | 28 likely | 22 conf. | 18 conf. | 26 conf. | 29 conf. |
| Free bytes at the tail | 1,299,232 | 191,440 | 259,456 | 5,797,104 | 768,592 | 365,808 |
bun bin/audio.js roms/<rom>.z64, except the last row, which is bun bin/cartridge.js roms/<rom>.z64. Notes and events are the totals that run prints, songs and effect banks together; songs and bytes of sequence count the real songs only. Command meaning reads confirmed / likely / no effect on the channel; the first two games have no such probe because their player has no command table of this kind. Bank assignment says whether the tool could tie a song to its bank from the game code or only by fit.Four things stand out, and three of them are one game standing alone:
- Revenge crams the most in. 316,742 bytes of sequence and 122,242 events — more than any of the three 32 MB games — out of a 16 MB cartridge with 2.9 MB of samples. Its songs run up to 19 channels against World Tour's 8.
- VPW2 is the sample monster. 8 banks, 516 samples, 12,504,447 bytes of VADPCM, and 7 effect banks against everyone else's 0, 1 or 2. Its third bank alone is ten samples of 46.323 s each, 7,504,056 bytes.
- No Mercy is the leanest sequencer. 39.6 minutes of music in 145,782 bytes, less than half of Revenge's, with a 486.1 s song in it.
- The first two games cannot be tied to their banks. Their bank assignment is likely, not confirmed: with no libmus player there is no binding in the code to read, so the tool matches songs to banks by fit and says so. It also notes that 19 of 19 and 28 of 28 would have had only one fitting bank anyway — which is a reason to believe it, not a proof.
One more thing the table does not show: none of the 142 songs and effect banks in the six games was found by a header check or a search threshold alone. Every one is reached through the file table, a constant in the code, or a binding the tool read out of the game. That is the Discovery path line, and it reports 0 for all six. The scan that found them is deliberately noisy and deliberately strict — in No Mercy it checked 33,554,417 ROM positions plus 10,683,656 positions inside 8,163 unpacked files, and rejected 183,207 candidates, each with a reason.
What you can do today
Open your own cartridge dump in AKI Anvil and pick the Music tab. Nothing is uploaded; the ROM is read in the browser. You get the song list with channels, notes, events and length, sortable; a detail pane with the tracks and the bank behind the song; a play button that renders the sequence with the tool's own sequencer and hands the result to WebAudio; a WAV download of what you just heard; and a play button on every sample in the bank.
bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64 # the summary above
bun bin/audio.js roms/*.z64 --songs # every song, with the commands it uses
bun bin/audio.js roms/*.z64 --banks # every sample: bytes, length, loop, codebook
bun bin/audio.js roms/*.z64 --commands # per command: args, meaning, the fields it pins
bun bin/audio.js roms/*.z64 --json # all of it, machine-readable
bun bin/audio.js roms/*.z64 --log # the full parse log, including every rejection
# render one song, or one sample, to a WAV you can open in a DAW
bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64 --song 5 --wav song5.wav
bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64 --sample 2:29 --wav theme.wav
What is missing, in the order I want it: MIDI export per song, so the music lands in a DAW instead of a WAV; a measured mixer, so a render stops being a good approximation and becomes a reproduction; and a write path, so a bank can be changed and put back. The last one is the subject of the next section.
Can you drop an MP3 in and make it a wrestler's theme?
This is the question I get asked, and it deserves an answer with arithmetic in it rather than a yes or a no.
The short version: the music is sequenced, so an MP3 can never become a sequence. But a theme does not have to be a sequence. It can be one long sample, played as one note — and three of these games already do exactly that. So the question is not whether the engine can, it is what it costs and where the bytes go.
Three things would have to be true. Here is each one against the measurement.
1. A sample long enough — confirmed, on the cartridge
The longest sample in VPW2 is 46.323 seconds, and it is not alone: that game's third bank is ten samples of 46.323 s, 750,429 bytes each, 7,504,056 bytes in total, every one of them looping over essentially its whole length. No Mercy's longest is 23.307 s and its third bank holds 43 samples, 42 with an endless loop, 403.8 seconds of audio in 6,541,578 bytes. WrestleMania 2000's third bank is 45 samples, all 45 looping, 401.9 seconds, 6,511,518 bytes. Nothing in the format caps a sample's length; the field is a 32-bit byte count.
2. The engine playing it as one note — confirmed, 200 times over
An effect bank is a table of very short sequences, one per sound. No Mercy's second effect bank has 200 entries and exactly 200 notes — one note each. All 200 read the same way: pick one wave, set one inline envelope, set pan, set volume, play the note, stop. 100 of them add a reverb setting and 50 chain a further effect; nothing else varies. The note lengths run up to 32,767 ticks, the largest a two-byte length can carry, which puts the longest entry at 341.9 seconds at the base tempo. WrestleMania 2000's second effect bank: 90 entries, 90 notes, over 45 samples — two per sample. VPW2 does the same with 20 entries over its ten 46-second samples.
$ bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64 --song 28 --entry 29 --seconds 40
render song 28 (bfx) with bank 2: 40.00 s, 4 notes, master 32767,
peak before clipping 1.673, clipped 46803 of 1764000 samples, 114 ms
warning 2.65 % of the samples are clipped (peak 1.673); master volume
32767 is applied; the summation and saturation of the RSP mixer are
a hypothesis
$ bun bin/audio.js roms/wwf_no_mercy_usa_rev_a.z64 --sample 2:29
sample 2:29 rate 28800 Hz, 671232 samples (23.307 s),
loop 52320..671200, peak 1.000
3. A place to put it — measured, and this is where it gets tight
bun bin/cartridge.js roms/<rom>.z64 reports the run of 0xFF padding at the end of each cartridge, and also checks that no address in the code points into it. Turn those bytes into seconds at 16,200 bytes per second and you get the whole budget:
| Cartridge | Free bytes at the tail | Seconds of VADPCM that fit |
|---|---|---|
| WrestleMania 2000 | 5,797,104 | 357.8 s |
| World Tour | 1,299,232 | 80.2 s |
| VPW2 | 768,592 | 47.4 s |
| No Mercy | 365,808 | 22.6 s |
| Revenge | 259,456 | 16.0 s |
| VPW64 | 191,440 | 11.8 s |
So: the honest answer
Yes, in principle, and the arithmetic is unforgiving. A theme as a sample is what these games already do, 200 entries deep in No Mercy. What you would be doing is replacing a sample, not adding a feature.
The budget is 16,200 bytes per second, flat. One megabyte is 64.7 seconds. A 30-second loop costs 486,000 bytes. A three-minute theme played straight through costs 2,916,000 bytes, and there is no cartridge here with three minutes of slack outside WrestleMania 2000. The way out is the way the games already take it: a short loop, held by one note for up to 341.9 seconds. That is why 42 of No Mercy's 43 long samples loop.
And the source has to be resampled to 28,800 Hz mono before any of it applies. The 3.556 : 1 compression is fixed, so a stereo theme costs exactly twice a mono one, and there is nothing to tune: no bitrate, no quality slider, no psychoacoustic model. An MP3 at 128 kbit/s is 16,000 bytes per second for stereo at 44.1 kHz. VADPCM wants 16,200 for mono at 28.8 kHz. That is the trade the N64 made in 1996, and it is why this format is still legible thirty years later.
The takeaway
The music in these games is written down, not recorded. A few hundred kilobytes of notes. A few megabytes of short samples. A player that turns one into the other sixty times a second.
That is why it still sounds like itself on a cartridge from 1997. It is also why you can read all of it back out. You just have to run the code instead of trusting the note someone left behind.
Methodology
Everything here was read from dumps of my own cartridges with AKI Anvil, on 11 September 2026. The command behind each figure is printed beside it; bun bin/audio.js roms/*.z64 --songs --commands produces nearly all of it in one run. No ROM data is reproduced on this page — only counts, offsets, rates and names. Structure names such as ALBankFile, PtrTablesV2 and the command names come from VPW Studio (MIT, AKI Club) and are not evidence in themselves: what each one does was measured separately, by running the game's handlers in a MIPS interpreter against the cartridge's own memory image. The same interpreter is the one used for the pin mechanic and the engine ranking.
| Cartridge | Code | SHA-256 |
|---|---|---|
| WCW vs. nWo: World Tour (USA, rev A) | NWNE | 3711c8838b18374d1c7be0192ad824082d33978f00b886d145bc55a782325d4d |
| Virtual Pro-Wrestling 64 (Japan) | NVPJ | 8b7191ae5489fc71c7a18c05a0afba035a88d8f22a2b047253dd9c524fb88921 |
| WCW/nWo Revenge (USA) | NW2E | 66c137d326565c6f31f992daba8f67c0aee7f025a142dd249d27019708014b60 |
| WWF WrestleMania 2000 (USA) | NWXE | cbd44033868d3747241f5028206056e64836d744c756b5c39dc7b3a446e8ce5b |
| Virtual Pro-Wrestling 2 (Japan) | NA2J | 358e9a345438155c6bd57da4bbf0f7a9fa1b4f7d5b1b726e8076c38f0f987e52 |
| WWF No Mercy (USA, rev A) | NW4E | fc561fce443010b114cc8ea41226772b7b2a2d33055be163fad91ac9c3b096cb |
bun bin/cartridge.js roms/<rom>.z64 prints the digest along with the header, the boot chip and the tail padding.