# Lane 6 — Retail MUSIC system (MediaMachine / MD_Data_Sound / winmm MIDI) Read-only research note. Sources: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR build, PDB-named), `acclient.h` (verbatim retail structs), `symbols.json`, plus `references/DatReaderWriter/`, `references/ACViewer/`, the retail install at `C:\Turbine\Asheron's Call\`, and the live `UserPreferences.ini`. --- ## 0. Headline finding — retail EoR HAS NO MUSIC SYSTEM This is the load-bearing result and it contradicts the standing assumption in `docs/research/deepdives/r05-audio-sound.md` §6. Three independent pieces of evidence, all from the exact PDB-paired 2013 build: 1. **`midiPlay` has zero callers.** The only three occurrences of address `0x00553390` in the whole 65 MB pseudo-C are the function's own definition/open/close lines (`acclient_2013_pseudo_c.txt:350072,350074,350133`). The single internal call is `midiPlayNext` → `midiPlay` (`:350147`), and `midiPlayNext` is itself only reachable from the `MidiProc` buffer-done callback (`:350223`) — i.e. it only ever advances a *queue that nothing ever fills*. 2. **Both MIDI callbacks are permanently null.** `midiEventCallback` and `midiStartCallback` are statically initialised to 0 (`:1185597`, `:1185598`) and there is no assignment site anywhere in the image. `MidiProc` null-checks them on every event and no-ops. 3. **No music preference and no music files.** `SoundManager::InitPrefs` / `ShutDown` register exactly eight sound preferences — `SoundDisabled`, `SoundVolume`, `AmbientSoundDisabled`, `AmbientSoundVolume`, `InterfaceSoundDisabled`, `InterfaceSoundVolume`, `SoundFeatures`, `PlaySoundOnlyWhenActive` (`:346764`–`:346810` region, Unregister list at `:00550367`–`:005503ad`). The live `%USERPROFILE%\Documents\Asheron's Call\UserPreferences.ini` `[Sound]` section contains exactly those keys — **no music volume, no music toggle**. And `C:\Turbine\Asheron's Call\` contains **zero `.mid` / `.rmi` / `.mp3` / `.wav`** files; the only media file on disk is `turbine_logo_ac.avi`. 4. **The word "music" does not appear anywhere in the 65 MB pseudo-C** (case-insensitive grep: 0 hits), and `SoundType` (the 0x00–0xCC enum, `acclient.h:4569`) has **no music member**. So: the client *links* a complete Microsoft-sample-derived SMF streaming player, initialises it at startup (`SoundManager::Init` → `midiSetup()`, `:346764`), tears it down at shutdown (`SoundManager::ShutDown` → `midiCleanup()`, `:346655`), and **never hands it a file**. It is dead infrastructure — a vestige of a 1999 design decision that was cut. What players actually hear as "music" in retail EoR is one of three things, all of them ordinary DAT `Wave` (0x0A) PCM played through the normal DirectSound path: | Perceived as | Actually is | Retail mechanism | |---|---|---| | Login / splash score | audio track of `turbine_logo_ac.avi` | `MD_Data_Movie` → DirectShow `IGraphBuilder` | | Dungeon "chanting/drums/whispers" atmosphere | UI SoundTable stingers | `CPlayerSystem::Handle_Admin__Environs` codes 0x65–0x7C | | Outdoor/dungeon soundscape | region ambient sound rolls | `Ambient` priority queue + `AmbientSTBDesc` | --- ## 1. The MIDI subsystem (documented for completeness / correction of r05) Free functions, all at `0x00552f60`–`0x00553840`. It is a near-verbatim port of Microsoft's `MIDIPLYR` SDK sample, including the literal event name `"Wait For Buffer Return"` (`:350321`). | Symbol | Addr | Role | |---|---|---| | `midiSetup()` | `0x00553770` | `midiOutGetNumDevs`, fill `dwVolCache[16]`/`dwVolPctCache[16]` with 100, `CreateEventA("Wait For Buffer Return")`, `midiStreamOpen(&hStream, &uMIDIDeviceID, 1, MidiProc, 0, 0x30000)`. Sets `MidiIsSetup`. | | `StreamBufferSetup(char* path)` | `0x00553030` | allocates **6 buffers × 0x400 bytes** (`LocalAlloc(LMEM_ZEROINIT, 0x423)` then 32-byte-aligned), `ConverterInit(path)`, `midiStreamProperty(…, 0x80000001)` = set time division, primes all 6 via `ConvertToBuffer` + `midiOutPrepareHeader` + `midiStreamOut`. | | `midiPlay(path, loop, immediate, tempoMul)` | `0x00553390` | if already playing and `immediate==0` → stash into `pending`/`pending_loop`/`dwQueuedTempoMultiplier`, set `is_pending`, return (that is the "queue next track" path). Otherwise `midiStop()`, `StreamBufferSetup`, then per-channel `midiOutShortMsg(0xB0|ch, ctrl 7, vol)` for all 16 channels, `midiStreamRestart`. `tempoMul` is a percentage (default `0x64` = 100). | | `midiPlayNext()` | `0x005534c0` | pops `pending` → `midiPlay(pending, pending_loop, 1, …)`. | | `MidiProc` | `0x00553500` | `MOM_DONE` (0x3C9) refills/rotates buffers mod 6; on end-of-data (`uCallbackStatus == 0x12C`) waits for all 6 buffers back, then either `midiPlayNext()` if a track is queued or `midiStop()`. `MOM_POSITIONCB` (0x3CA) sniffs the stream: caches controller-7 volume per channel and forwards note-on/off + volume on **channels 14/15 only** to `midiEventCallback` (a game-sync hook — never installed). | | `midiStop()` | `0x00553240` | `midiStreamStop`, `midiOutReset`, `WaitForSingleObject(hBufferReturnEvent, 0x7D0 = 2000 ms)`, `ConverterCleanup`, `FreeBuffers`, close+reopen the stream, reset `is_pending`/`pending_loop`/`dwQueuedTempoMultiplier = 100`. | | `SetChannelVolume(ch, pct)` | `0x00552f60` | `midiOutShortMsg(0xB0|ch, ctrl 7, dwVolCache[ch]*pct/100)`. The only volume control; **no fade, no crossfade, no ramp anywhere**. | | `ConverterInit(path)` | `0x00554530` | `CreateFileA(path, GENERIC_READ, …)` → reads `'MThd'` (`0x6468544D`), byte-swaps header, `dwFormat`/`dwTrackCount`/`dwTimeDivision`, then per track reads `'MTrk'` (`0x6B72544D`) into a 0x400 window. **Standard MIDI File from a loose disk path — never from a DAT.** | ### Corrections to `r05-audio-sound.md` §6 | r05 claim | Verdict | |---|---| | "Music is MIDI, streamed through midiStreamOpen" | **Correct as to mechanism**, wrong as to it being used. | | 6 × 1024-byte buffers, "Wait For Buffer Return" event, 16-channel volume arrays | **Confirmed.** | | "MThd/MTrk parsing at FUN_00555150" | Right idea, wrong address in the named build: `ConverterInit` @ `0x00554530`. | | "pan (0x0A)" via `midiOutShortMsg` | **Not found.** Only controller **7 (volume)** is written (`SetChannelVolume`, `midiPlay`). No pan CC. | | "Track selection is by the game code calling `PlayMusic(path, loop)` — driven by region/area rules" | **REFUTED.** No such caller and no region→track table exists. This sentence is the source of the `PlayMusic(string resourceName, bool loop)` shape in `IAudioEngine` — that signature is an invention, not a retail port. | | "Recommended: convert MIDI to OGG offline" | Moot — there is no MIDI content to convert. | **Consequence for the port:** a MIDI synth is *not* needed. Neither is an OGG music bus. There is nothing to be faithful to. --- ## 2. `MediaMachine` — what it actually is (a per-UI-element media bytecode VM) `MediaMachine` is **not** the music system. It is the interpreter for the *media script* attached to every UI element **state** in the LayoutDesc DAT. Playing a sound is one of its eleven instructions. ### Ownership chain ``` LayoutDesc (DBObj) acclient.h:33881 └─ ElementDesc : StateDesc acclient.h:33693 └─ StateDesc acclient.h:33640 └─ SmartArray m_media ← the script UIElement └─ MediaMachine m_mediaMachine acclient.h:33786 ├─ UIElement* m_owner ├─ SmartArray m_array ← deep copy of the active state's m_media └─ unsigned m_curIndex ← the program counter ``` `MediaMachine : UIListener` (`acclient.h:33873`). ### State machine (this is the whole thing) - `MediaMachine::Reset(const SmartArray&)` @ `0x00465d90` (`:112681`): `Cleanup()`, then deep-copy every `MediaDesc` via `MediaDesc::CreateMediaType(const MediaDesc*)`, set `m_curIndex = 0`, and immediately `Update()`. Called from `UIElement::SetState` → `MediaMachine::Reset(&m_mediaMachine, &m_desc.m_media)` (`:108863`), and on element copy (`:111688`, `:111745`). - `MediaMachine::Update()` @ `0x00465ba0` (`:112526`) — the interpreter loop: 1. `UIListener::UnRegisterForGlobalMessage(this, 3)`. 2. While `m_curIndex < m_array.m_num`: dispatch on `m_type - 1` through an 11-entry jump table (`jump_table_465cc0`, `:112621`) to the matching `Update_X(desc)`. 3. **The return value is "may I advance?"** — non-zero ⇒ `m_curIndex++` and continue in the same call; **zero ⇒ break** (the instruction is still blocking). 4. On break, `UIListener::RegisterForGlobalMessage(this, 3)` — i.e. subscribe to the per-tick global message so the machine resumes next frame. - `MediaMachine::ListenToGlobalMessage(msg, _)` @ `0x00465cf0`: `if (msg == 3) Update()`. **Global message 3 is the machine's clock.** There is no dedicated music/media tick. - `Cleanup()` @ `0x00465af0`: virtual-deletes every owned `MediaDesc`, zeroes the array. Called by dtor and by `Reset`. Termination: the machine runs off the end of the array and stops (no re-registration). A `Jump` instruction is what makes a script loop forever. ### Instruction set (`MediaDesc::m_type`, 1-based) Verified from `MediaDesc::CreateMediaType(uint32_t)` @ `0x0069d420` (`:675786`) and each ctor's `MediaDesc::MediaDesc(this, N)`: | # | Type | Struct (acclient.h) | Blocking? | Semantics | |---|---|---|---|---| | 1 | Movie | `MD_Data_Movie` :34160 — `PStringBase m_strFileName`, `bool m_StretchToFullScreen`, `MovieTheatre*` | yes, until done | `MD_Data_Movie::Update(owner)` @ `0x0069d489`; DirectShow (`ATL::CComPtr` :34191). Requires owner visible-bit `(m_owner+0x554) >> 0x11 & 1`. | | 2 | Alpha | `MD_Data_Alpha` :34118 — `DID m_file` | no | alpha mask image | | 3 | Anim | `MD_Data_Anim` :34101 — `float m_duration`, `m_drawMode`, `SmartArray m_frames`, `double m_StartTime`, `int m_displayedFrameNum` | yes, for `m_duration` | flipbook; latches `m_StartTime` on first visit (sentinel `-1.0`) | | 4 | Cursor | `MD_Data_Cursor` :34168 — `DID m_file`, `int m_xHotspot`, `m_yHotspot` | no | `UIElement::SetCursor` | | 5 | Image | `MD_Data_Image` :34111 — `DID m_file`, `m_drawMode` | no | set the element's picture | | 6 | Jump | `MD_Data_Jump` :34132 — `uint m_jumpItemIndex`, `float m_probability` | no | `RollDice(0,1)` vs probability; on pass `m_curIndex = m_jumpItemIndex - 1` (then the loop's `++` lands exactly on `m_jumpItemIndex`). **This is the loop primitive.** | | 7 | Message | `MD_Data_Message` :34139 — `uint m_messageID`, `float m_probability` | no | `UIElement::BroadcastElementMessage(owner, m_messageID, 0, 0)` | | 8 | Pause | `MD_Data_Pause` :34124 — `float m_minDuration`, `m_maxDuration`, `double m_endTime` | **yes** | first visit: `m_endTime = Timer::compute_time() + RollDice(min,max)`; blocks until now ≥ endTime, then resets `m_endTime = -1.0` | | 9 | **Sound** | `MD_Data_Sound` :34146 — `DID m_file`, `SoundType m_stype` | no | see §3 | | 10 | State | `MD_Data_State` :34153 — `uint m_stateID`, `float m_probability` | terminal | probabilistic `owner->SetState(m_stateID)`; **always returns 0** (`:112068`) so the machine stops — the new state's `Reset` takes over | | 11 | Fade | `MD_Data_Fade` :34176 — `float m_startAlpha`, `m_endAlpha`, `m_duration`, `double m_startTime` | **yes** | see §4 | Sentinel convention: `m_StartTime` / `m_endTime` / `m_startTime` use the double `-1.0` (`0xBFF00000` in the high word) as "not yet started"; each blocking instruction resets it to `-1.0` when it completes, so a `Jump` back over it re-arms it. ### Text/serialised form `MediaDesc::ToFileNode` / `CreateFromFileNode` (`0x0069d740` / `0x0069d7e0`) read a `MediaType` node (keyword `"MediaType"`, `KW_MEDIATYPE` @ `:821851`) under a `"Media"` node (`KW_MEDIA` @ `:821841`), enum-name table 14. Per-type keywords seen in the init block around `:821780`–`:821960`: `MinDuration`, `MaxDuration`, `Probability`, `SoundName`, `SoundTable`, `StartAlpha`, `EndAlpha`, `Duration`, `StateID`, `StretchToFullScreen`, `NoDBFile`, `PassToChildren`. `StateDesc::LoadMedia` @ `0x0069c950` (`:674969`) appends each parsed desc; `StateDesc::ConcatenateMedia` @ `0x0069c9b0` merges a parent state's script into a child's. `DatReaderWriter` already models all of this: `references/DatReaderWriter/DatReaderWriter/Generated/Types/MediaDesc.generated.cs` (abstract + `MediaType` dispatch) and `MediaDescSound.generated.cs` (`uint File`, `Sound Sound`). **The binary layout is `int32 mediaType, int32 type, uint32 file, uint32 sound`** — note the doubled type field, which the generated reader reproduces. --- ## 3. `MediaMachine::Update_Sound` — the authored-sound instruction `0x004658b0` (`:112264`). Full decode: ``` Update_Sound(MD_Data_Sound* d): if (d == null || m_owner == null) return 0; // 0 = block (dead-end) if (d->m_stype == Sound_Invalid) // 0 # m_file is a direct Wave DID if (d->m_file.id != 0) SoundManager::PlaySoundFromCenter(d->m_file, 1.0f) # volume literal 0x3F800000 return 1 else # m_file is a SoundTable DID; look it up as DBO type 0x22 CSoundTable* st = DBObj::Get(QualifiedDataID(d->m_file.id, 0x22)) if (st != null) SoundManager::PlaySoundFromCenter(d->m_stype, st) return 1 return 1 # falls through, still advances ``` Two shapes, discriminated by `m_stype`: - `m_stype == Sound_Invalid` ⇒ **`m_file` is a `Wave` DID (0x0A……)**, played at literal volume 1.0 via the `PlaySoundFromCenter(DID, float)` overload (`0x005509e0`, `:346951`) which looks the wave up in `SoundManager::sound_hash_`. - `m_stype != Sound_Invalid` ⇒ **`m_file` is a `SoundTable` DID (0x20……)** and `m_stype` selects the row; `PlaySoundFromCenter(SoundType, CSoundTable*)` (`0x00550950`, `:346927`) rolls `SoundManager::GetSound` (weighted pick over `SoundTableData::data_[]` with priority/probability/volume) and plays it. DBO types confirmed: `CSoundTable::GetDBOType() == 0x22` (`:349100`), `DBWave::Get()` uses `QualifiedDataID(id, 0xF)` (`:349327`). **Both go through the *interface* channel**, not a music channel: `PlaySoundFromCenter` gates on `SoundManager::interface_sounds_enabled` and `s_bPlaySoundOnlyWhenActive && Device::m_bIsActiveApp`, then `GetAttenuation(0f, vol, &out, /*isAmbient=*/0)` and `PlaySoundInternal(buf, /*position=*/0, vol)` — position 0 ⇒ non-positional, "from center". So the UI/authored-media bus == the interface-sound bus, and the retail Interface Sound Volume slider is its only volume control. This is the closest thing retail has to "the client plays a piece of authored audio because a UI state was entered" — and it is the mechanism the login screen and any title-card audio would use. --- ## 4. Fades — the only fade math in the media system (and it is alpha, not audio) `MediaMachine::Update_Fade` @ `0x00465930` (`:112307`): ``` now = Timer::compute_time() target = owner->m_object ?? owner->m_parent->GetObjectA() # the drawable if (d->m_startTime == -1.0) d->m_startTime = now # latch on first visit dur = d->GetDuration() # == m_duration if (fabs(dur) >= 0.000199999995f) # EPSILON = 2e-4 s t = (now - d->m_startTime) / dur else t = 1.0f t = clamp(t, 0.0f, 1.0f) # two-sided clamp alpha = d->m_startAlpha + (d->m_endAlpha - d->m_startAlpha) * t # plain lerp target->vtable[0x48](alpha) # set element alpha if (t >= 1.0f) { d->m_startTime = -1.0; return 1 } # done → advance return 0 # still fading → block ``` Notable: **linear** interpolation, no easing; duration epsilon `2e-4 s`; a zero/short duration snaps to `endAlpha` in one tick. **There is no audio fade, no crossfade, and no volume ramp anywhere in `MediaMachine` or in the MIDI code.** `SetChannelVolume` is an instantaneous CC-7 write. Any crossfade in an acdream music feature would be new design, not a port. --- ## 5. Trigger map — every code site that starts "music-adjacent" audio | Event | Site | What plays | |---|---|---| | UI element enters a state | `UIElement::SetState` → `MediaMachine::Reset` (`:108863`) → `Update_Sound` (`:112589`) | authored `MD_Data_Sound` (Wave DID or SoundTable+SoundType) | | Per-tick continuation of a blocked script | `MediaMachine::ListenToGlobalMessage(3)` → `Update` (`:112642`) | next instruction in the script | | UI element cleanup / hide | `MediaMachine::Cleanup` (`:109993`), `Update` (`:110007`) | — (stops the script, does **not** stop already-playing sounds) | | Splash / intro | `MD_Data_Movie::Update` (`:675489`) → DirectShow on `m_strFileName` | `turbine_logo_ac.avi` (its audio track *is* the theme) | | Teleport start (portal in) | `gmSmartBoxUI` teleport anim, `:218903` | `Sound_UI_EnterPortal` (0x6A) from `ClientUISystem::GetUISoundTable()` | | Teleport end (portal out, `TAS_WORLD_FADE_IN`) | `:219745` | `Sound_UI_ExitPortal` (0x6B) | | **Server-pushed atmosphere cue** | `CPlayerSystem::Handle_Admin__Environs(uint)` @ `0x0055de20` (`:362360`+) | codes below | | Landblock / cell change | `CellManager::ChangePosition` @ `0x004559b0` → `Ambient::InitSounds` (`:94714`), `LScape::add_ambient_sounds` (`:94718`), `Ambient::UpdatePlayQueue` (`:94722`), `Ambient::ReleaseSoundTables` (`:94726`) | region ambient rolls (§6) | | Per-frame ambient pump | `Ambient::UseTime` @ `0x00551880`, called from `:94200` | due entries from the ambient PQueue | ### `Handle_Admin__Environs` — the retail "set the mood" opcode `AdminEnvirons` (acdream: `0xEA60`, `WorldSession.cs:1915`). Two disjoint ranges: - `1..6` → lighting/fog overrides (`LScape::m_override_*`: ambient level, ambient colour, fog colour, fog min/max; case 6 also sets `m_bRadarBlank = 1`). acdream already ports these. - `0x65..0x7C` → **one-shot UI-SoundTable stinger**, each `PlaySoundFromCenter(Sound_UI_*, GetUISoundTable())`, gated on the player physics object existing: | code | SoundType | | code | SoundType | |---|---|---|---|---| | 0x65 | `Sound_UI_Roar` (0x76) | | 0x6E | `Sound_UI_Drums` (0x7F) | | 0x66 | `Sound_UI_Bell` (0x77) | | 0x6F | `Sound_UI_GhostSpeak` (0x80) | | 0x67 | `Sound_UI_Chant1` (0x78) | | 0x70 | `Sound_UI_Breathing` (0x81) | | 0x68 | `Sound_UI_Chant2` (0x79) | | 0x71 | `Sound_UI_Howl` (0x82) | | 0x69 | `Sound_UI_DarkWhispers1` (0x7A) | | 0x72 | `Sound_UI_LostSouls` (0x83) | | 0x6A | `Sound_UI_DarkWhispers2` (0x7B) | | 0x75 | `Sound_UI_Squeal` (0x84) | | 0x6B | `Sound_UI_DarkLaugh` (0x7C) | | 0x76–0x7A | `Sound_UI_Thunder1..5` (0x85–0x89) | | 0x6C | `Sound_UI_DarkWind` (0x7D) | | (0x7B/0x7C) | tail of the Thunder run | | 0x6D | `Sound_UI_DarkSpeech` (0x7E) | | | | Codes `0x73`/`0x74` have no case (fall through, nothing plays). Note the environ code and the `SoundType` are **offset by 0x11** but not uniformly — the switch is explicit, so port it as an explicit table, never as arithmetic. This is *the* trigger for what players remember as dungeon "music". --- ## 6. Region ambient soundscape — retail's real "area audio" Not music, but it is the system a "music by region" feature would have to sit next to, and it is the only region→audio authored data in the DATs. ``` CRegionDesc (acclient.h:53230) └─ CSoundDesc* sound_info :53200 — SmartArray CSceneType :53240 — { name, scenes[], AmbientSTBDesc* sound_table_desc } AmbientSTBDesc :35486 — { DID stb_id, bool stb_not_found, SmartArray ambient_sounds, CSoundTable* sound_table, uint play_count } AmbientSoundDesc :35496 — { SoundType stype, bool is_continuous, float volume, float base_chance, float min_rate, float max_rate } ``` - Selection is **per land cell, from the terrain word**: in `CLandBlock::add_ambient_sounds` (`:314270`+) each cell reads `terrainType = (t >> 2) & 0x1F` and `sceneIdx = t >> 11`, then `CRegionDesc::GetSTBDesc(region, terrainType, sceneIdx)` → `Ambient::AddSound(ambient, stbDesc, cellVertexPos)` (`:314293`). - `AmbientSound` is polymorphic: `ConstantSound` (continuous, tracks `current_volume`) and `IntermitSound` (per-`LandDefs::Direction` `min_dist[8]` / `max_dist[8]` arrays + `play_chance`) — `acclient.h:52830`, `:52856`. - Scheduling is an absolute-deadline priority queue: `Ambient::Play` @ `0x005517a0` (`:347826`) → `CanHear()`, `PlayNow()`, `GetSoundPos()`; positional ⇒ `PlayAmbientSound`, non-positional ⇒ `PlayAmbientSoundFromCenter`; then `PQueueArray::Insert(sound_queue, Timer::cur_time + GetPlayInterval(), snd)` and `on_queue = 1`. `Ambient::UseTime` pops due entries. - Ambient volume path: `PlayAmbientSoundFromCenter` @ `0x005508b0` multiplies by `SoundManager::ambient_sound_volume`, rolls `rand() * 3.05185094e-05f` (= `1/32768`) against the entry probability, then `GetAttenuation(0, vol, &out, /*isAmbient=*/1)`. --- ## 7. Cross-reference results - **`references/ACViewer/`** — grep for `midi|music` over all `*.cs`: only three hits, all unrelated (`WeenieClassName.cs`, `SoulEmote.cs`). **ACViewer implements no music and no MIDI.** It is not an oracle here, and its silence is itself corroboration that there is nothing to load. - **`references/DatReaderWriter/`** — no music/MIDI DBObj type exists. Audio surface is exactly two DBObjs: - `Wave` — `DBObjType.Wave`, range **`0x0A000000`–`0x0A00FFFF`**, layout `int32 headerSize, int32 dataSize, byte[] header, byte[] data` (`DBObjs/Wave.generated.cs`). The `header` is a `WAVEFORMATEX` blob; the body can be PCM **or MP3** (retail decodes via winmm ACM). - `SoundTable` — `DBObjType.SoundTable`, range **`0x20000000`–`0x2000FFFF`**, `int32 HashKey`, `Dictionary`, `Dictionary` with `SoundEntry { QualifiedDataId Id, float Priority, Probability, Volume }`. - `MediaDesc*` types are all present (§2), including `MediaDescSound`. - **There is no `0x25……` music table and no MIDI DAT type.** (`0x25……` is the `RegionDesc`/`Region` family, not music.) --- ## 8. Answers to the five questions **1. Formats and where the bytes live.** Retail's *only* music-capable path is winmm `midiStream*` fed a **Standard MIDI File read from a loose disk path via `CreateFileA`** — never from a DAT. No DirectMusic, no MP3-as-music, no `0x25……` music table. That path is never invoked and **no `.mid` ships with the client**. All audio the client actually plays is DAT `Wave` (`0x0A000000`–`0x0A00FFFF`, PCM or MP3-in-WAVEFORMATEX) selected either directly by DID or through `SoundTable` (`0x20000000`–`0x2000FFFF`). The one long-form musical asset that ships is the audio track of `turbine_logo_ac.avi`, played by DirectShow. **2. The MediaMachine state machine.** It is not a music machine — it is an 11-opcode media bytecode VM per UI element state, with `m_curIndex` as PC, "may I advance?" booleans as the blocking protocol, global message 3 as the clock, `Jump` as the loop primitive, `State` as the terminal instruction, and `-1.0` double sentinels for "not yet armed". Full opcode table and per-opcode decode in §2; `Update_Sound` in §3; `Update_Fade` (linear alpha lerp, 2e-4 s duration epsilon) in §4. **3. Trigger map.** §5. Region/landblock entry drives *ambient*, not music. Portal in/out and the 24 `AdminEnvirons` codes `0x65–0x7C` drive the atmosphere stingers. UI state entry drives authored `MD_Data_Sound`. Login/ intro music is a movie file. Nothing anywhere starts a music track. **4. Does a modern port need a MIDI synth?** **No.** There is no MIDI content and no code that would play it; EoR-era audio is 100% sampled. ACViewer implements nothing here either. Building a synth (or an OGG music bus) would be new feature design with no retail referent — and per CLAUDE.md that is a "different feature", not a port, so it needs explicit approval. **5. Rough port scope.** - **Delete the false surface (smallest, highest value).** `IAudioEngine.PlayMusic(string resourceName, bool loop)` / `StopMusic()` in `src/AcDream.Core/Audio/AudioModel.cs:102-103`, their no-op bodies in `src/AcDream.App/Audio/OpenAlAudioEngine.cs:386-387`, and `MusicVolume` at `AudioModel.cs:84` / `OpenAlAudioEngine.cs` are modelled on a retail feature **that does not exist**. `resourceName` as a *string path* is itself a tell — every other audio entry point in the engine is DID-keyed. A retail-faithful engine has three buses (SFX / Ambient / Interface), not four. Removing them retires a divergence rather than creating one; if they stay, they need a `retail-divergence-register.md` row explaining that they model dead retail code. Also worth a look: `src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs:260` comments the music path as "stubbed for R5 MIDI", and retail's Settings has no music slider at all. - **Correct the record.** `docs/research/deepdives/r05-audio-sound.md` §6 and its executive-summary table row ("MIDI music | winmm midiStream | Loose `*.mid` files on disk") should carry the "infrastructure present, never invoked, no content ships" finding — see the correction table in §1. That table row is what produced the phantom API. - **The genuinely missing retail behavior, in cost order:** 1. **`AdminEnvirons` sound cues** (24 codes → `Sound_UI_*` via the UI SoundTable). acdream already parses these and already has `RuntimeEnvironmentSoundCue`, with `src/AcDream.Runtime/World/RuntimeWorldEnvironmentState.cs:214-239` logging `audio binding pending`. This is a table + one `PlayUi` call — the cheapest real retail-fidelity win in the whole lane. 2. **`MediaDescSound` in the LayoutDesc importer.** `src/AcDream.App/UI/Layout/LayoutImporter.cs:464-489` reads `MediaDescImage` and `MediaDescCursor` from each state's media list and ignores everything else. Adding the Sound opcode (two shapes per §3, both routed to the interface bus) gives retail's authored UI sounds for free — `DatReaderWriter` already parses the type. 3. **Region ambient system** (§6): `AmbientSTBDesc` selection from the terrain word on landblock change, `ConstantSound`/`IntermitSound`, the absolute-deadline PQueue, `ambient_sound_volume`. This is the real "area audio" and it is what `StartAmbient`/`StopAmbient` (`OpenAlAudioEngine.cs:367-385`, currently handle-reservation only) exist to serve. Multi-commit; belongs in a roadmap phase, not an issue. 4. **`MediaMachine` proper** (Pause/Jump/Anim/Fade/State scripting) — only if animated UI states become a goal. Not required for audio. - **Explicitly out of scope for a faithful port:** MIDI playback, a soundfont synth, crossfades, a music bus, region→track tables. None exist in retail. --- ## 9. Anchors (for citation in code comments) | Symbol | Address | pseudo-C line | |---|---|---| | `SoundManager::Init` (calls `midiSetup`) | `0x00550640` | 346753 | | `SoundManager::ShutDown` (calls `midiCleanup`) | `0x005502b0` | ~346580 | | `midiSetup` | `0x00553770` | 350282 | | `midiPlay` (**no callers**) | `0x00553390` | 350072 | | `midiPlayNext` | `0x005534c0` | 350138 | | `midiStop` | `0x00553240` | 349978 | | `midiCleanup` | `0x00553350` | 350047 | | `MidiProc` | `0x00553500` | 350152 | | `StreamBufferSetup` | `0x00553030` | 349843 | | `ConverterInit` (MThd/MTrk) | `0x00554530` | 351254 | | `SetChannelVolume` | `0x00552f60` | 349787 | | `midiEventCallback` / `midiStartCallback` (= 0) | `0x0086fa70` / `0x0086fa74` | 1185597–8 | | `MediaMachine::Update` | `0x00465ba0` | 112526 | | `MediaMachine::Reset` | `0x00465d90` | 112681 | | `MediaMachine::Cleanup` | `0x00465af0` | 112475 | | `MediaMachine::ListenToGlobalMessage` | `0x00465cf0` | 112638 | | `MediaMachine::Update_Sound` | `0x004658b0` | 112264 | | `MediaMachine::Update_Fade` | `0x00465930` | 112307 | | `MediaMachine::Update_Pause` | `0x00465520` | 111937 | | `MediaMachine::Update_Jump` | `0x004655b0` | 111983 | | `MediaDesc::CreateMediaType(uint)` | `0x0069d420` | 675786 | | `MD_Data_Sound::MD_Data_Sound` (type 9) | `0x0069e5f0` | 677163 | | `MD_Data_Sound::Serialize` | `0x0069e670` | 677192 | | `StateDesc::LoadMedia` | `0x0069c950` | 674969 | | `SoundManager::PlaySoundFromCenter(SoundType, CSoundTable*)` | `0x00550950` | 346927 | | `SoundManager::PlaySoundFromCenter(DID, float)` | `0x005509e0` | 346951 | | `SoundManager::PlayAmbientSoundFromCenter` | `0x005508b0` | 346893 | | `CPlayerSystem::Handle_Admin__Environs` | `0x0055de20` | ~362360 | | `Ambient::Play` | `0x005517a0` | 347826 | | `Ambient::UseTime` | `0x00551880` | 347957 | | `CellManager::ChangePosition` (ambient re-init) | `0x004559b0` | 94601 | | `CSoundTable::GetDBOType` (= 0x22) | `0x00552560` | 349100 | | `DBWave::Get` (QDID type 0xF) | `0x00552880` | 349327 | | `enum SoundType` (0x00–0xCC) | — | `acclient.h:4569` | | `MediaMachine` / `MediaDesc` / `MD_Data_*` structs | — | `acclient.h:33873, 33907, 34101–34182` |