Cracking Bally Sente Name That Tune (1986)
How we decoded 1,003 polyphonic melodies, 6-voice arrangements, and drum patches from a 1986 arcade cabinet built around six CEM3394 analog synth chips
April 18, 2026 · crack, bally-sente, 6809, nametune, synth
Name That Tune
Bally Sente · 1986 · SAC-1 · Motorola 6809 + 6× CEM3394 analog synths · 1,003 polyphonic songs
The Cabinet
Bally Sente’s Name That Tune1 (1986) is the rarest and most technically ambitious of the Trivia-era Sente cabinets. Unlike its cousins (Trivial Pursuit, Sente Diagnostic), it was a musical game: play a melody, have the player bid how few notes they need to name the song, then reveal the answer with a full arrangement. Every song on the machine, 1,003 of them, was stored as a polyphonic 6-voice stream on six analog synth chips designed by Sequential Circuits (of Prophet-5 fame).
Nobody had ever extracted the music. The ROMs were dumped in MAME but the format was opaque: not MIDI, not General MIDI, not anything with a public spec. Just 256 KB of undocumented bytes and a 6809 interpreter spread across a bank-switched memory map.
The Game Flow
Four stages, all visible to the player:
- Category, a clue is shown on screen (“BEATLEMANIA”, “60’S TOP TEN”)
- Melody cue, the tune plays on one voice, no accompaniment
- Bid, players dial down how few notes they can name it in
- Reveal, if correct, the song replays with full 6-voice arrangement
The three buttons in each mini-player above mimic those stages:
- ▶ bid, the opening 5 notes of ch0, what the game uses for the bid phase
- ▶ tune, the full melody on ch0, no accompaniment (what plays during bidding)
- ▶ full, all 6 voices, the reveal arrangement
Every sound you’re hearing is being rendered live in your browser from the original arcade ROM bytes, no pre-rendered audio files, no MIDI conversion. Just the exact same opcode stream that the 6809 ran in 1986, played through WebAudio synths approximating the CEM3394’s role.
The Data
All 1,003 songs are playable in the browser:
- Name That Tune Melody Workbench , pick any song, toggle voices, hear the arrangement
- Name That Tune Q&A entries , category, correct answer, decoys, all extracted
- Source code , extractor, disassembler, tests
How We Cracked It
ROM Layout
Name That Tune ships on 8× 32 KB ROM chips, totaling 256 KB:
nttab01.bin Song index + category table (0x0000-0x2E3A)
+ song entries with pointers & titles (0x2E3A-0x7FFF)
nttab23.bin Song entries continued (0x0000-0x349E) + melody data
nttab45.bin Melody streams
nttab67.bin Melody streams
nttcd01.bin Melody streams
nttcd23.bin Melody streams (busiest chip, 710+ song streams)
nttcd45.bin Melody streams + game data
nttcd6ef.bin 6809 game code ($E000-$FFFF in CPU space)
The 6809 CPU only sees 64 KB of address space at a time. It swaps
chips into view by writing a bank index to $9F00, a hardware
latch on the SAC-1 board that re-maps which physical ROM is visible
at which CPU address.
Bank Switching, The Static Crack
Finding the right ROM chip for a given song was the first real blocker.
Each song entry has six 3-byte pointers: (bank, addr_hi, addr_lo).
The bank field is not a ROM chip index, it’s an argument to a
lookup routine at $FC9D that computes the real $9F00 value.
Disassembling $FC9D in nttcd6ef.bin:
$FC9D: 58 ASLB ; D <<= 4 (four shifts total)
$FC9E: 49 ROLA
$FC9F: 58 ASLB
$FCA0: 49 ROLA
$FCA1: 58 ASLB
$FCA2: 49 ROLA
$FCA3: 58 ASLB
$FCA4: 49 ROLA
$FCA5: 48 ASLA ; idx = D >> 11 (5 bits)
$FCA6: 8E FC D9 LDX #$FCD9 ; table base
$FCA9: A7 ... STA $9F00 ; write bank register
The table at $FCD9 has 32 entries (5 bits of index), each a pair of
(high_flag, bank_value). Walking all 1,003 songs through this shift-
and-lookup reduced to a clean per-bank formula with exactly two
exceptions out of 1,003:
# Bank 0, addr_hi >= $A0 → nttab23, offset = addr & 0x7FFF
# Bank 1, addr_hi < $80 → nttab45, offset = addr
# Bank 1, addr_hi >= $80 → nttab67, offset = addr & 0x7FFF
# Bank 2, addr_hi < $80 → nttcd01, offset = addr
# Bank 2, addr_hi >= $80 → nttcd23, offset = addr & 0x7FFF
# Bank 3, any → nttcd45, offset = (addr + 0x2000)
Six rules. 1,001 songs hit one of them. The remaining two (THE
ENTERTAINER at bank 1 $4A74, BLACKBIRD at bank 1 $63D9) route
through an alternate switcher at $FD30, so they get a two-entry
_PER_SONG_OVERRIDE hand-patch and the extractor is done with
banking forever.
Title Encoding, Base-40 Again
Bally Sente reused the base-40 radix encoding from Trivial Pursuit, but
with a dual-mode twist: every 2-byte word auto-switches between
“3 chars in 2 bytes” (the TP encoding) and “2 chars in 2 bytes” (a
dense mode for 2-letter words, using a shifted range 0xE29A-0xE8F9).
We documented the TP encoding fully in Cracking Bally Sente Trivial Pursuit , the NTT variant just adds the mode switch per word. Both character sets, both parsing rules, decoded in about 15 lines of Python. All 1,003 titles extracted cleanly.
The Melody Stream, 16 Opcodes
The real challenge. Each song is a stream of bytes interpreted by an
interrupt-driven scheduler at $F0E8. Bytes with the high bit clear
(0x00-0xEF) are pitch bytes followed by a duration; bytes with
the high bit set (0xF0-0xFF) are control opcodes. Sixteen of
them, each with its own handler:
| Op | Bytes | Meaning |
|---|---|---|
F0 xx | 2 | Set tempo (writes to global $0550) |
F1 xx | 2 | Loop start, xx iterations |
F2 xx | 2 | Program change (select CEM3394 instrument patch) |
F3 xx yy | 3 | JSR: target = F3_addr + xxyy (3-deep call stack) |
F4 | 1 | RTS (pop call stack) |
F5 xx | 2 | Direct register write |
F6 | 1 | Song end (calls $F57C which nulls ALL channel pointers) |
F9 xx yy | 3 | Combined register + value write |
FA | 1 | Loop end (counted) |
FB xx yy | 3 | Register + value write |
FC xx | 2 | Transpose accumulate (base += xx) |
FD | 1 | Clear transpose (base = 0) |
FE | 1 | Channel end (nulls this channel’s Y pointer) |
FF | 1 | Banked jump to per-song init |
F3 / F4 are the big deal, melodies heavily use JSR into shared
subroutines. A song’s ch0 stream might be 10 bytes long and do
nothing but set a tempo, pick a program, and JSR into a 50-byte
melody body reused from elsewhere in ROM. The F3 call stack is 3
deep, F1 loops are 3 deep. Every song is a tiny 6809 program in its
own right.
Implementing the simulator, parse_melody_events(), took one
afternoon and most of a second one chasing off-by-ones. Once it was
right, every pitch byte landed on a real note.
The 6-Pointer Revelation
This one cost us the most time, because our first reading of the format was wrong.
Each song entry stores 18 pointer bytes, six 3-byte tuples. Our initial guess (from an old code comment): three difficulty levels, each a start/end pair. L1 = easy, L2 = medium, L3 = hard.
That reading kinda-sorta worked. It produced a melody. The notes were in the right ballpark. Tests passed. But some songs had extra notes that didn’t belong, and switching difficulty levels didn’t sound like a real difficulty change. Something was off.
The truth, found by disassembling the song-load routine at $F8A3:
$F8A3: 10 8E 01 41 LDY #$0141 ; Y points to RAM buffer
$F8A7: CC 06 00 LDD #$0600 ; (unrelated init)
...
$F8BD: 30 01 LEAX 1,X ; advance ptr in song entry
$F8BF: BD FD 19 JSR $FD19 ; resolve bank → $9F00
$F8C2: A6 80 LDA ,X+
...
$F8D6: ED A1 STD ,Y++ ; store resolved address
$F8DA: 26 E1 BNE $F8BD ; 6 iterations
Then the channel-init at $F4DA:
$F4E0: CE 05 52 LDU #$0552 ; U = channel 0 state block
$F4F0: 10 AE 81 LDY ,X++ ; Y from RAM buffer
$F4F3: 10 AF C4 STY ,U ; write to state[0,1]
$F4FD: 33 C8 19 LEAU +25,U ; advance to next channel
$F514: 25 D8 BCS $F4EE ; 6 iterations
The six “difficulty pointers” are actually six channel Y-pointers
, one per CEM3394 chip. The 6809 reads all six into RAM buffer
$0141+, bank-resolves each one, then copies them into the six
channel state blocks at $0552 + ch*25.
ch0 terminates with F6 (song end, which nulls all six channels).
ch1-5 terminate with FE (channel end, which only nulls that one
channel’s pointer). Ch0 is always the melody. ch1-5 are
accompaniment.
Our old code was scanning for FE markers to infer channel starts, which shifted every channel index by one and entirely missed ch1. After the fix, 1,003/1,003 songs extract cleanly with six proper voices each.
Click ▶ full and listen: ch0 melody on sawtooth, ch1 harmony on triangle, ch2 rhythm on square, ch3 bass doubled an octave down, ch4 pad, ch5 melody doubled, a full 6-voice polyphonic arrangement, same as the arcade played in 1986.
The Byte-Sharing Trick
One of the most elegant things in the ROM is how adjacent songs share bytes.
Every channel stream begins with a 2-byte “init padding” prefix the
scheduler skips (state[2] is initialized to 1; the first tick does
DEC state[2]; BEQ; LEAY 2,Y, advancing 2 bytes before reading
any opcode). So the first two bytes of each song’s ch0 are never
executed as opcodes.
But they are executed as something, specifically, as the tail of the previous song’s shared subroutine.
Look at JINGLE BELLS ch0 in nttcd23.bin:
$035A: 40 F4 F0 04 ; init-pad | F0=tempo 4
$035E: F2 17 ; program change $17 (lead voice)
$0360: F3 00 55 ; JSR +$55 (target = $03B5)
$0363: F6 ; song end
That JSR lands in the shared pool at $03B5, which is where the
actual 26-note melody lives. The subroutine runs through a loop,
emits its pitches, and returns with an F4 (RTS), at which point
ch0 hits F6 and the song ends.
But here’s the trick: ch0’s RTS is at $03E6, and the next
song (DIANE) starts at $03E5. The two bytes $03E5: 0x20 and
$03E6: 0xF4 serve double duty:
- For DIANE, they are init padding, skipped by the scheduler
- For JINGLE BELLS’ subroutine, they are the last note’s duration byte and the RTS that returns to F6
The same two bytes of ROM are both the end of one song and the beginning of the next. You can’t insert or remove songs without breaking their neighbor’s subroutine. Every song is interlocked with its neighbors in ROM in a perfect pack.
This is why our extractor reads a 2000-byte buffer starting at each song, well past where the next song “starts”, so the JSR simulator can follow calls into the shared pool and terminate correctly on the RTS → F6 chain.
Global Tempo, Why Hey Jude Dragged
First pass of our extractor had the melody voice playing at one tempo and the accompaniment playing at a different tempo. Hey Jude especially: the lead dragged 50% behind the drums. It sounded cursed.
The fix came from the F0 opcode handler at $F1D9:
$F1D9: F7 05 50 STB $0550 ; store tempo globally
$F1DC: F7 05 4F STB $054F ; initialize countdown
$F1DF: 31 22 LEAY 2,Y ; advance past F0 xx
$F1E1: 16 FF 34 LBRA $F118 ; back to main loop
Tempo is global. One value at $0550, used by every channel.
The main ISR entry at $F0D5 does DEC $054F; LBNE $F2B8.
counting down the tempo divider before any channel processes its
next opcode.
In practice, only ch0 ever emits F0. ch1-5 inherit ch0’s tempo via the global register. Our simulator was tracking tempo per-voice and defaulting to 8 when no F0 was seen, so Hey Jude’s ch0=F0(12) set itself to tempo 12 while ch1-5 stayed at our default 8. Result: melody 1.5× slower than accompaniment.
Every channel now uses ch0’s F0 value. Try ▶ full, lead and accompaniment lock together.
Decoys, Random, Not Curated
The arcade shows four titles: one correct, three wrong. We wondered whether the decoys were a hand-picked table per song, or randomly drawn from the category.
Disassembling the decoy-selection loop at $F814:
$F814: BD F6 72 JSR $F672 ; get random song index
$F817: ... ; store at $01F5 (the correct answer slot)
...
$F830: BD FA 88 JSR $FA88 ; get another random index
$F833: B1 01 F5 CMPA $01F5 ; collide with correct?
$F836: 27 F8 BEQ $F830 ; yes → try again
$F838: B1 01 FA CMPA $01FA ; collide with decoy 1?
$F83B: 27 F3 BEQ $F830 ; yes → try again
...
Random from the category’s song list, with retry-on-collision, a
classic pick-3-without-replacement loop. The PRNG at $F672 seeds
from $0095/$0096, shifts the seed, masks to 3 bits, and MULs
into the category list.
Our extractor seeds random.Random(song_title) (stable per song,
different across songs) and samples 3 from the category. This
matches the arcade’s behavior faithfully, decoys vary per song,
don’t repeat, and stay stable across reruns.
Voices, Six CEM3394 Chips
The Bally Sente SAC-1 sound board is a marvel. Six CEM3394 analog synthesizer chips, the same chips Sequential Circuits used in the Six-Trak, Multi-Trak, and Max polysynths. Each chip has 8 control-voltage inputs: VCO frequency, waveform select, pulse width, filter cutoff, filter resonance, modulation, VCA gain, and mixer balance.
The F2 opcode selects a program (a preset CV bundle). Running
through the F2 values per channel across all songs:
| Channel | Common F2 values | Role |
|---|---|---|
| ch0 | 11, 23, 24, 69, 72 | Lead / melody |
| ch1 | 17, 20 | Counter-melody, harmony |
| ch2 | 1, 2, 3 | Percussion (short-decay patches) |
| ch3 | 1, 2, 3 | Percussion |
| ch4 | 1, 3, 25 | Bass |
| ch5 | 23, 25, 27 | Pad / melody doubling |
Values 1-3 on ch2/ch3 are the closest thing to drums. Analog
synths don’t have a sample-based drum chip, instead they use
short-attack, fast-decay envelopes on a square wave to approximate
kick drums and rim hits. Our web player detects F2 ≤ 5 on
ch≥2 and swaps in a Tone.MembraneSynth (pitched kick drum) for
those voices.
Listen for the drum thumps on the full arrangement, that’s ch2 and ch3 firing percussion patches. ch0 carries the melody; ch5 often doubles it at a different octave for presence.
The Extractor
End-to-end pipeline in scripts/bally_sente/nametune.py
:
- Read all 8 ROM chips
- Scan
nttab01/nttab23for FF-delimited song entries - Decode each title via the dual-mode base-40
- For each song, read the 6 channel pointers
- Bank-resolve each via the static formula
- Read a 2000-byte buffer and run
parse_melody_eventsfor each channel’s Y-pointer - Capture note events (midi, duration), F2 program changes, and the F6 end marker on ch0
- Truncate ch1-5 at ch0’s F6 tick count (models the
$F57Ckill-all-channels behavior) - Emit JSON with per-voice
{ch, tempo, prog, n: [notes]}
The test suite at scripts/tests/test_nametune_melodies.py locks
down four hand-verified songs (JINGLE BELLS, GREENSLEEVES, EENSY
WEENSY SPIDER, THE ENTERTAINER) against a golden reference, exact
interval sequences, exact note counts, the works. 17 tests. All
pass.
What’s Still Open
One mystery remains: the note counter. The game lets the player bid “name it in N notes” for any N, not a fixed set of levels. Somewhere there’s a counter that stops melody playback after N notes so the player can answer. We searched the entire 6809 EF bank and couldn’t find it, no per-note decrement in the scheduler, no bid-value write to RAM, no comparator against a small integer.
Three possibilities:
- The counter lives in the Z80 sound CPU (we haven’t disassembled it deeply)
- The bid is declarative only, melody plays continuously and the player interrupts by pressing the answer button; the bid is just compared against elapsed-notes for scoring
- A non-EF 6809 bank we haven’t touched
Option 2 is the most plausible, and it’s consistent with arcade UX of the era. But we haven’t proven it.
And a parting listen, one of the shortest songs in the ROM, and one of the longest, to hear the range:
Thanks to MAME for the ROM dumps, to Sequential Circuits for making the CEM3394 in 1980, and to whoever at Bally Sente thought it was a good idea to store 1,003 polyphonic melodies as a custom 16-opcode bytecode. You were right.
References
Related
More cracks
Cross-archive analyses
Name That Tune (1986, Bally Sente) · Arcade-Museum · Flyer · MAME romset:
nametune↩︎