-
Notifications
You must be signed in to change notification settings - Fork 0
MM2 Music Format
Navigation: Home - Audio - MM2 Music Format
Might and Magic II does not play sampled music or .wav files for title, walk, or combat cues. Sound is Amiga Paula tone synthesis via audio.device, driven from:
- A 6240-byte (
0x1860) master blob (resource namemaster.32, index 9 in the filename table at0x0212../ DATA0x07B0). -
Runtime tables copied from that blob into the
A4workspace duringaudio_init(0x823C). -
Sequencer routines in CODE hunk 0 (and overlay helpers) that read song steps and call note/tone helpers through the
JSR d(A4)thunk table (A4 = $7FFE).
Resolved directly from the A4 thunk table in EXTRACTED/hunks/mm2_data_00.bin:
-
JSR -$7C62(A4)-> code0x218EA -
JSR -$7C44(A4)-> code0x21D80 -
JSR -$7BAE(A4)-> code0x22C24(opcode dispatcher) -
JSR -$7BA8(A4)-> code0x22D68 -
JSR -$7B9C(A4)-> code0x22EAA
Dispatcher (0x22C24) opcodes include direct jumps for:
-
0x04->-$7BA8(A4) -
0x08->-$7B9C(A4)
The string master.32 lives in the executable at code offset 0x264 (DATA pointer table index 9). On a full install the loader opens that file; the logical format is the 0x1860 byte stream read by Read() into RAM. The player code (init, Paula wrappers, thunks) is in the CODE hunk; pitch tables and the 60×16-step song bank are not usable until audio_init has parsed master.32 into A4-$79DE, A4-$796A, and A4-$4F4C. A few small tone-dispatch tables (spell/combat message stingers) are embedded in CODE near 0xF5BC / 0x13C5E.
flowchart LR
master32["master.32 0x1860 bytes"]
init["audio_init 0x823C"]
bss["A4 tables: -79DE periods, -796A note map, -4F4C songs"]
evt["Event handlers title / combat / wipe"]
thunk["JSR d A4 thunks -7B9C -7BA8 -7C62 -7BDE"]
paula["Overlay Paula 0x25842 / 0x25926"]
master32 --> init --> bss
evt --> thunk --> paula
bss --> thunk
All paths below assume A4 = $7FFE and audio_init has run (also re-run from 0x86D8 when the music refresh timer fires). A4-$79B2 is the Sounds flag (1 = off): title music is skipped when Sounds is on @ 0x1000.
| User-visible moment | Caller (CODE offset) | Audio entry | Indices / ids | Data used |
|---|---|---|---|---|
| Main title |
0x0FEE / 0x1028 (menu setup); overlay 0x601C
|
JSR -$7B9C play_song
|
Song 0x12D, channel 0x47, level 0xE0
|
Blocking score; overlay driver 0x22EAA + Paula |
| Title ambience loop |
0x1FF6–0x2014 (72 steps) |
JSR -$7BA8 song_step
|
Song 0x12D, tempo 0xE8, step -$24(A5)
|
Steps from A4-$4F4C (bank loaded from blob +0x780) |
| Title UI chirp | 0x1052 |
JSR -$7C62 play_note
|
Note 0x11 (17) |
A4-$796A → A4-$79DE |
| Combat enter |
0x12C6E … 0x12E42 (encounter setup) |
JSR -$7C62 + channel 0x16
|
Note 0x2B (43); voice alloc @ 0x12D42
|
Same note map; optional play_tone if many monsters |
| Combat round | 0x12A22 |
(no fanfare) | Sets A4-$79B2 = 2; channel 0x0C/0x10 @ 0x12A5A
|
UI/collision only; stings come from message helper |
| Monster turn sting |
0x12848 via 0x12B3C
|
JSR -$7C62 / -$7BDE
|
Notes 17 or 32, 29, 31+slot, 47; HP-scaled tone | Periods from A4-$79DE after index lookup |
| Combat victory |
0x12430 when live monster count 0 @ 0x12C54
|
JSR -$7C62 × 23
|
Note 5 (twice × 0x17 loops) |
Victory jingle = repeated note 5 |
| Party wipe / combat defeat |
0x9F22 (all dead); 0x11660 (combat_defeat_retreat) |
JSR -$7E96 → 0x7DCC
|
Voices 0x31/0x32/0x33 on ch 6–8; play_tone on roster gold / age / level fields |
Multi-voice funeral tones, not a single play_note index |
-
0x1000: ifA4-$79B2 != 1, after channel setup the game callsplay_songwith0x12D(0x1014–0x1028). -
0x1FF6: non-blockingsong_steploop —0x48iterations, same song id and tempo0xE8— drives the title from the song bank atA4-$4F4C, not a simple 0..59 index. -
0x64F8(play_song_scripted): usesA4-$73C4as a pointer table (script lines +0x6798wait); song id0x12Dis an index into that table, mainly for text + blocking score elsewhere (e.g. scripted scenes @0x78E6), not the same as one row of the 60-song bank.
Overlay mirror: 0x22FF6 area (0x601C, 0x22FE6 0x12D, 0x22FFC song_step via thunk -$7BA8).
Encounter setup clears combat BSS, saves/restores Sounds, builds the monster list, then:
- Allocates a music channel
0x16(0x12D9E–0x12DA2, thunks-$7C74/-$7C56). - If more than 10 monsters (
0x12E1E), playsplay_note0x2Bthen aplay_tonewhose duration argument ismonster_count - 10(0x12E42–0x12E58).
Entering the round loop (0x12A22) only forces A4-$79B2 = 2 and refreshes the combat display — no victory/defeat melody until 0x13282 returns and 0x12C54 / 0x12C66 branch.
-
Victory —
0x12430: after treasure/XP work,play_notewith index5in two loops of0x17notes (0x12508–0x12544), plus palette/text helpers on channel0x26. -
Defeat / retreat —
0x11646:JSR -$7E96(same thunk as party wipe) into0x7DCC, which prints “Gather… / …has died!” and assigns three voice types0x31–0x33, thenplay_toneon each survivor’s$66(gold),$5C, or$25(level) depending on type.
When the party is eliminated outside the victory path (0x9F22 after 0x9F0A combat check):
- Same
0x7DCChandler as defeat: noplay_noteindex — three programmable voices + HP/gold/age-scaled tones. - Earlier in the wipe sequence (
0x9F6E–0x9FF0) XP is applied usingA4-$796Anote indices vs0x18threshold (game logic, not a sting).
Typical play_note (JSR -$7C62, e.g. 0x205E8 spell path):
- Stack: note index (e.g. victory
5, walk0x2D, combat enter0x2B). -
word = A4-$796A[index](must be <0x18for audible slot). -
Period =
A4-$79DE[word](andA4-$79CAfor some dual-voice paths). - Channel
0x20(32) for beeps,0x26(38) for longer music lines — viaJSR -$7BFCthen Paula.
play_tone (JSR -$7BDE): stack duration, channel, period word (often roster field or scaled HP).
Spell/combat message lines can bypass raw indices via the CODE jump table 0xF5BC (8 words, e.g. $FF5C…$FFD0) before JSR -$7F3E.
The string master.32 lives in the executable at code offset 0x264 (DATA pointer table index 9). On a full install the loader may still open a separate file with that name; the logical format is the 0x1860 byte stream read by Read() into RAM, not planar graphics.
| Step | Address | What happens |
|---|---|---|
| Party/resource init | 0x3290 |
Read 0x1860 bytes from A4-$883A into A4-$2A3E (per-character record buffer base; also used as load buffer here). |
| Audio init | 0x823C |
Alloc 0x1860 bytes; on failure call open helper (-$843A); Read blob into alloc buffer; parse into A4 tables (below). |
| Reload | 0x86D8 |
Same init when A4-$7A48 timer exceeds A4-$79EE (re-parse). |
Blob parsing uses:
-
0x815A—memcpyfrom blob base + cursor into a destination buffer. -
0x8192— read big-endian u16 at cursor (packed offset in the stack argument), advance cursor. -
0x8176— bulk copy variant used for large blocks (song bank).
All destinations are A4 offsets (true signed displacements from $7FFE). Init order:
| Blob read | Size | Runtime A4
|
Role |
|---|---|---|---|
| 10 × u16 | 20 | -$79DE |
Pitch / period table A (10 words) |
| 10 × u16 | 20 | -$79CA |
Pitch / period table B |
| 8 × u16 | 16 | -$796A |
Note index → table A lookup (used by play_note) |
| u16 | 2 | -$795A |
Voice/channel count cap (cmp in voice alloc) |
| u16 | 2 | -$79B6 |
Related timing / era hook |
| u16 | 2 | -$79B4 |
Related timing |
| u16 | 2 | -$7972 |
Global transpose / music flag |
| u16 | 2 | -$7970 |
Global transpose / music flag |
| u16 | 2 | -$796E |
Global transpose / music flag |
offset 0x780
|
1920 | -$4F4C |
Song bank: 60 songs × 16 steps × 2 bytes (u16 per step) |
| 10 bytes | 10 | -$7995 |
Small byte table |
| 24 bytes | 24 | -$798B |
Per-key / walk-related byte flags |
| 4 bytes | 4 | -$79A4 |
Small word table |
| 1 byte | 1 | -$79B2 |
Sounds on (title skips music when == 1) |
| 1 byte each | … |
-$79B0..-$7996
|
Walk SFX / UI flags |
Song stride: song_index * 0x20 + step * 2 → u16 at A4-$4F4C (asl #5 on song index in init and playback).
play_song_step logic (overlay/title path uses JSR -$7BA8):
- Form index:
(screen_mode << 5) + (coord_nibble << 1)into song bank. - Load step word; mask with
A4-$75EE(coord-dependent enable table). - If zero, rest.
- Else decode low byte:
(byte >> 2)indexesA4-$762E; OR in channel/mode bits from screen id (0x29..0x2C).
-
Gate:
A4-$79B2(Sounds)cmpi #1— if Sounds on, title tune is skipped (0x1000). -
Blocking score:
JSR -$7B9C(play_song) with args including song id0x12D(0x1000..0x1028). -
Overlay step loop:
0x1FF6..0x2014— 72 (0x48) calls toJSR -$7BA8with song0x12D, step counter, tempo0xE8.
0x12D is a script / score id (also used with pointer table A4-$73C4 @ 0x64F8), not a simple 0..59 index into the 60-song bank.
-
JSR -$7C62— play note by index (stack: note number, e.g. walk0x2D, victory5, death17/29/31). - Index maps through
A4-$796Ainto period tableA4-$79DE. -
JSR -$7BDE— play tone with duration + period word (scaled on monster death @0x12848). - Channel
0x26(38) music;0x20(32) short beeps.
Vectors cached at A4-$48C / A4-$490; wrappers at 0x25842 / 0x25926 forward to audio.device (start/stop/set period/volume).
Jump-offset table at 0x13C5E / 0xF5BC (8 entries, words like $FF5C, $FF6E, …) — dispatch table for spell/combat message tones, not raw periods.
The code hunk contains all drivers; the 0x1860 master blob is loaded at runtime via the same path as master.32. A static scan of EXTRACTED/mm2 hunks (CODE+DATA+overlay) does not expose an obvious contiguous 6240-byte instance on this copy (no master.32 on MM2BOOT either). Tools should:
- Accept
--master pathfor a captured0x1860file. - Optionally scan a raw binary for a candidate (heuristic in
tools/mm2_music.py).
See EXTRACTED/decomp/mm2_music.h and tools/mm2_music.py (MasterMusic dataclass).
The current build has a repeatable per-event call-path exporter:
- Script:
tools/export_mm2_event_audio_paths.py - Inputs:
EXTRACTED/mm2.capstone.asm,EXTRACTED/tmp_mm2_thunk_map.txt - Outputs:
-
EXTRACTED/mm2_event_audio_paths.json(all events in one bundle) EXTRACTED/audio-events/title.jsonEXTRACTED/audio-events/combat_enter.jsonEXTRACTED/audio-events/combat_victory.jsonEXTRACTED/audio-events/party_death_or_defeat.json
-
Each event JSON includes:
- callsite address
- thunk slot (
JSR -$xxxx(A4)) - resolved target function (from thunk map)
- nearby stack push context (immediates/register pushes into the call)
- normalized
literal_stack_argswhere immediate words andCLR.Wpushes are decoded to integers
For MOD conversion prep (track patterns + short SFX extraction), use:
- Merged evidence doc:
EXTRACTED/docs/26-audio-callpaths-title-death-shared.md - Exporter script:
tools/export_mm2_mod_audio_intermediate.py - Output JSON:
EXTRACTED/mm2_mod_audio_intermediate.json
Current schema (schema_version: 3) adds:
-
event_families[]with per-family confidence (proven/likely/uncertain) - dual-view targets at family and event levels:
-
static_target(thunk-table/static map view) -
observed_behavior_target(runtime behavior/traced-entry view)
-
-
events[]with per-event confidence,source_addresses[], andparameter_provenance[] - deterministic coverage for title start/step/poll, death tone dispatch (including
-$7BD8as likely), walk-beep candidates (note 0x2Dcallsites), and short-note metadata buckets. - corrected deterministic counts in current export:
counts.families = 4counts.events = 18-
counts.short_note_callsites = 232(corrected from prior overcounted inventory) counts.walk_beep_immediate_0x2d_callsites = 7
Confidence interpretation in contradictory thunk cases:
- proven: static and observed target agree, with direct instruction evidence.
-
likely: evidence is strong but static-vs-observed target differs (notably
-$7E96(A4)). - uncertain: kept for unresolved semantics/targets not yet normalized.
Usage:
python tools/export_mm2_mod_audio_intermediate.pyPrototype script:
tools/build_mm2_mod_prototype.py
Input/output:
- Input JSON:
EXTRACTED/mm2_mod_audio_intermediate.json - Output MOD:
EXTRACTED/mm2_prototype.mod - Sidecar map:
EXTRACTED/mm2_prototype.pattern_map.json
What it writes (deterministic minimal ProTracker subset):
- 4 channels (
M.K.signature) - fixed 31 sample headers (starter bank in sample slots 1..5, rest empty)
- pattern table + packed 64-row pattern data
- sample payload bytes (tiny looping waveforms)
Prototype mapping strategy:
-
Pattern 0 (
provenfocus):-
title.song.start_blockingat row 0/ch 0 -
title.loop.song_stepas stepped pulses rows 4..34/ch 1 - death proven dispatch/tone anchors (
pick_type_31_33,type32,type33)
-
-
Pattern 1 (
likelyfocus):- title poll previews (
poll_mode0,poll_mode1) death.dispatch.type31_call_7BD8- walk-beep family (
note 0x2D) callsites as short beeps
- title poll previews (
Current limitations / assumptions:
- This is a playable scaffold, not a byte-accurate reconstruction of Paula runtime behavior.
- MM2 note/tone semantics are approximated to MOD periods with a simple deterministic mapping.
- Title loop uses a representative step slice (first 16 pulses) rather than full game-time sequencing.
- Proven and likely events are intentionally separated by pattern block to keep confidence explicit.
-
EXTRACTED/docs/27-mm2-known-songs-catalog.md— current known song/SFX catalog with confidence tags -
EXTRACTED/docs/06-gfx-loading.md— filename table (master.32index 9) -
EXTRACTED/docs/14-game-state-struct.md—A4workspace / thunks -
EXTRACTED/docs/01-startup-init.md—A4 = $7FFE
- Docs Wiki Hub ← full doc index
- Getting Started ← read this first
- Overview
- Workspace Notes
- Open Questions
- Full Analysis
- Game Remake
- Startup and Init
- Runtime Memory Map
- Main Loop and Map
- Exploration Input and Options
- Party and Session
- Game State Struct
- Data Hunk mm2 data 00
- Time Era Calendar
- GFX Loading
- ANM TV Format
- PC DOS graphics
- SNES graphics
- 3D View and Game Screen
- Amiga 3D Render Process
- Scripted Scene Graphics
- Event Graphics Opcodes
- Title Screen Assets
- Title Screen Animation
- Format inventory
- items.dat
- monsters.dat
- roster.dat
- attrib.dat
- map.dat
- spells & item use
- event.dat
- Events by location ← 71 maps
- Events hub (numbered)
- Event Script Opcodes
-
Event Script DSL ←
.mm2evtauthoring - Event Text Rendering
- Event Graphics Opcodes
- Combat Overview
- Combat System
- Encounter Tables
- Spell Cast ASM
- monsters.dat abilities
- Spells and item use
- Town Services
- Spell Sources
- Character Mechanics
- Skills and Hirelings
- Commerce Formulas
- Commerce World Services
- Mount Farview Class Quest
- Class Quest HP Bug
- Event Runtime
- Event to String Path
- Embedded Exe Strings
- Copy Protection
- Time Era Calendar
- Game State Struct
- MM1 Overview ← hub + decode status
- MM1 MAZEDATA format
- MM1 to MM2 outdoor
- MM1 WALLPIX by sector
- MM1 art & graphics
- MM1 items / monsters (status)
- MM1 map walker ↗
- MM1 2D maps gallery
- MM1 WALLPIX gallery