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


← Archive

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.

JINGLE BELLS
HEY JUDE
YELLOW SUBMARINE

The Game Flow

Four stages, all visible to the player:

  1. Category, a clue is shown on screen (“BEATLEMANIA”, “60’S TOP TEN”)
  2. Melody cue, the tune plays on one voice, no accompaniment
  3. Bid, players dial down how few notes they can name it in
  4. Reveal, if correct, the song replays with full 6-voice arrangement

The three buttons in each mini-player above mimic those stages:

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:


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:

OpBytesMeaning
F0 xx2Set tempo (writes to global $0550)
F1 xx2Loop start, xx iterations
F2 xx2Program change (select CEM3394 instrument patch)
F3 xx yy3JSR: target = F3_addr + xxyy (3-deep call stack)
F41RTS (pop call stack)
F5 xx2Direct register write
F61Song end (calls $F57C which nulls ALL channel pointers)
F9 xx yy3Combined register + value write
FA1Loop end (counted)
FB xx yy3Register + value write
FC xx2Transpose accumulate (base += xx)
FD1Clear transpose (base = 0)
FE1Channel end (nulls this channel’s Y pointer)
FF1Banked 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.

JINGLE BELLS, six voices now

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:

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.

HEY JUDE, tempo aligned

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:

ChannelCommon F2 valuesRole
ch011, 23, 24, 69, 72Lead / melody
ch117, 20Counter-melody, harmony
ch21, 2, 3Percussion (short-decay patches)
ch31, 2, 3Percussion
ch41, 3, 25Bass
ch523, 25, 27Pad / 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.

ABA DABA HONEYMOON
YELLOW SUBMARINE

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 :

  1. Read all 8 ROM chips
  2. Scan nttab01/nttab23 for FF-delimited song entries
  3. Decode each title via the dual-mode base-40
  4. For each song, read the 6 channel pointers
  5. Bank-resolve each via the static formula
  6. Read a 2000-byte buffer and run parse_melody_events for each channel’s Y-pointer
  7. Capture note events (midi, duration), F2 program changes, and the F6 end marker on ch0
  8. Truncate ch1-5 at ch0’s F6 tick count (models the $F57C kill-all-channels behavior)
  9. 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:

  1. The counter lives in the Z80 sound CPU (we haven’t disassembled it deeply)
  2. 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
  3. 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:

HERE COMES THE BRIDE (short)
MIAMI VICE (long, dramatic)

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


More cracks

Cross-archive analyses


  1. Name That Tune (1986, Bally Sente) · Arcade-Museum · Flyer · MAME romset: nametune ↩︎