Skip to content

Sound: how WINPOD.EXE makes it, and the port ​

POD's sound comes from Ubi Soft's own sound library, linked into the game (srcson\pentium\sndplay.cxx, sound3d.cpp in its assertion strings). It mixes 16-bit stereo at 22,050 Hz on a thread of its own and hands blocks to waveOut. Music is not mixed: the CD drive plays the disc's audio track through MCI.

The latest POD release is the only current reference (AGENTS.md). This page began with an earlier address map; the current executable's mapped labels are in labels/pod/names.tsv and measure/sound_builds.py. The sound measurements were repeated against POD's WINPOD.EXE, and pod-sound follows POD where the historical comparison identified differences: the choice of ten channels and the stereo-swap flag at start (docs/versions.md, "Sound: first edition against POD"). Recordings of the running game are historical evidence from the first edition; current measurements use POD.

The port is crates/pod-sound. State: measured for every layer below the game's triggers: the mixer, the channel calls, the 3D parameters, the events and the engine sound give the original's numbers sample for sample under GameFrame's 32-bit emulator. Compared with a recording of the running game for the menu sound: the same samples within ±1 in 94% of them, the rest the recording path's; and for a race's countdown and engines, replayed from a step log of the same race ("Compared with the running game"). The triggers themselves belong to the sessions porting the code that fires them.

LayerOriginalPortEvidence
Outputsound thread FUN_004c7605, waveOut set up in FUN_004c5bf0host (playback)observed
MixerFUN_004df88a, gain FUN_004df859mixer, fpumeasured: 400 cases, 215,044 frames
ChannelsFUN_004c70d8, FUN_004c6db8, FUN_004c6f28, FUN_004c6fd4, FUN_004c7018, FUN_004c7464, FUN_004c5974channelsmeasured: 300 sequences, 18,744 calls
Sounds in spaceFUN_004bf5c8, FUN_004bf38c, FUN_004bf41c, FUN_004bf4fc, FUN_004db73dspacemeasured: 5,000 + 5,000 cases
Events and enginesFUN_004bfa5c, FUN_004c00f0, FUN_004bfd2c, FUN_004bfc98, FUN_0045f410eventsmeasured on FIX.MEG: 150 cases, 3,553 frames, 3,943 blocks
BanksFUN_004c1e48, FUN_004c0f3c, FUN_004c0d8c, FUN_004c19c0, FUN_004c6518events::Sounds::load, enableobserved
MusicFUN_004c50a8, FUN_004c51ec, FUN_004c5154, FUN_004c31e0, FUN_004c4170, FUN_004c49e0music, cddaobserved; unit-tested

Output ​

FUN_004c5bf0 opens waveOut with PCM, 2 channels, 22,050 Hz, 16 bits, 4 bytes a frame (0x697E50: 1, 2, 0x5622, 0x15888, 4, 16; observed). The format is fixed: no setting changes it. The output is 100 blocks of 880 bytes (0x4F9808, 0x4F9810): 220 frames, about 10 ms each. All 100 are queued at once, and each MM_WOM_DONE (message 0x3BD) makes the thread (FUN_004c7605) run FUN_004c5974 and mix fresh blocks until 25 are ahead of the one playing (0x4F980C), so a sound starts about 250 ms after the call that asks for it (inferred from the counts). Two debug routines, FUN_004c8142 and FUN_004c8660, measure the latency with other block sizes and report it in message boxes; nothing calls them in play (inferred).

The thread mixes under the library's mutex 0x69D354; every channel call takes the same mutex (observed). The port runs the calls between blocks, in the order they are made.

The mixer ​

FUN_004df88a (EAX the byte count, ESI the output block; observed, measured) mixes into the next block of a ring at 0x698E10 .. 0x698E14 that holds twice the 100 output blocks, then copies the block out:

  1. Clear the block, 8 bytes at a time.
  2. For each of the 64 channels (0x69B238, 0x84 bytes each) that plays: count the block in the channel's history (+0x50 + slot × 4), apply the pitch slide (step += slide while the countdown at +0x74 lasts), then either mix it (when +0x40, "audible", is set) or only move its position by bytes × step / 4.
  3. When the environment 0x697E34 is not 0, add the ring 12,008 bytes back (0x4F983C), halved, in place: a feedback echo of about 136 ms.

One multiply for both sides. FUN_004df859 stores a channel's gain as one double, vol × pan × 2^32 + vol × (127 − pan). The mixer multiplies each sample by it and adds 3 × 2^65 (0x4FB3F0), so the stored double's mantissa holds the product in units of 2^14: bits 0 to 15 are the right side's word, bits 32 to 47 the left's. Both routines start with fnsave, which resets the FPU to round to nearest at 64-bit precision, and the sum is rounded twice (to the 80-bit register, then to the stored double). The port does it in integers (fpu.rs); with one rounding instead of two, case 24 of the measurement is one step off (measured). Two side effects of the trick: a negative right-side value borrows from the left word, so a sound panned fully right puts −1 into the left side; and the pan range is 0 to 127 with 127 meaning left.

The pipelined loop. The channel loop multiplies the sample at the current position, steps, and adds the product only when the next position is still before the end. So the last sample before the end of a sound, or before a loop restarts, is never heard, and the sample in flight when the block fills is skipped (observed at 0x4df983 .. 0x4df9f4, measured). Positions are 24.8 fixed point in samples. Each side saturates on its own (jo, then the carry gives the sign).

The echo halves each side by an arithmetic shift, then adds both sides as one 32-bit word, so a carry out of the left word reaches the right word; each side then saturates on its own overflow (0x4dfa90 .. 0x4dfac1, measured). It reads behind the block and wraps at the ring's start.

Channels ​

FUN_004c70d8 starts a sampled sound on the first channel that is not playing (EAX the sound, EDX 8 bytes: +0 pitch 16.16, +4 volume, +5 pan, +6 send; ECX a callback, EBX an owner word). The channel's volume is param × sound volume >> 7, its step sound factor × pitch >> 16 with the factor (rate << 8) / 22050 set at load (FUN_004c0f3c), and it is audible when the volume is 3 or more. With stereo off (0x4F9814 = 0) every pan is 0x40; with 0x698E18 set, pans are 128 − pan. The gain is computed last (observed in the disassembly, measured).

FUN_004c6db8 changes a playing channel's volume, pan and send at once and glides its pitch. The average number of blocks mixed between the last eight updates (+0x50 .. +0x6C, summed and shifted right by 3) sets how long: slide = (avg + new − old) / avg when rising, (new − avg − old) / avg when falling, and the countdown (new − old) / slide (observed, measured). The decompiler shows these two fields swapped. The history slot +0x70 is never wrapped: on its eighth step the store meant to clear hist[8] lands on +0x70 itself and resets it to 0, leaving hist[0] uncleared (measured: the test fails when the slot is masked).

FUN_004c5974 runs before each mix and marks at most ten channels audible: those with a volume above 3 that play and are not paused, in table order; once ten are chosen, a louder channel replaces the first chosen one that is quieter than it, not the quietest (observed, measured). POD (FUN_004be26c) replaces the quietest instead, the first of equals (measured); pod-sound does as POD. FUN_004c6fd4 and FUN_004c7018 pause and resume a channel, clearing its slide, countdown and history. FUN_004c7464 swaps left and right for every channel without recomputing the gains.

Sounds of kind 3 and 4 do not use a channel: FUN_004c73a8 returns pseudo-channel 0x42 (a CD track) or 0x44 (a music script), and the update, stop and query calls treat 0x41 to 0x44 apart.

Sounds in space ​

FUN_004bf5c8 (EAX the object, EDX the 8 bytes, EBX flags) fills the parameters of a sound at an object for the listener (observed, measured):

FlagParameterHow
2Volume0x7F0 / ((d + 16.0) >> 16), at most 127, with d the octagonal distance: per pair of axes the larger plus half the smaller, x with y, then that with z
1PitchFUN_004bf38c: `(300·
4PanFUN_004bf41c: the horizontal offset along the listener's side axis 0x4F80E4, divided by the octagonal distance, mapped piecewise to 0 .. 127; 0x40 within 1/256 unit
8SendFUN_004bf4fc: the same along the front axis 0x4F80D8, from a point 4 units ahead; 0 .. 127

Without a flag the volume is 127, the pitch 1.0, the pan 0x40 and the send 0. The send (+0x44) is read only by the MMX build's mixer; WINPOD's ignores it (observed).

FUN_004db73d, the length of a vector, sums the squares on the x87 like FUN_004db530 (adding 1.5 × 2^68 and rounding at each step), then takes the square root from a table that FUN_004cfde4 fills at start-up: entry k (0x2000 .. 0x7FFF) is ⌊√k × 65536⌋. The sum is normalised into the table's range by an even shift and the result shifted back by half of it (observed, measured). From a length of 2^31 the index turns negative and the original reads memory before the table; the sound code never measures that far.

The listener (FUN_004c00f0, each frame): the camera's position (0x60BBA0; the player's car in view mode 3), the player's car's velocity (zero in view mode 2), and two of the camera's axes (0x60BBC4 front, 0x60BBAC side). Until the first frame the axes hold (0, 1, 0) and (1, 0, 0) from the image (measured: the first sounds' pan depends on it).

Events ​

The game asks for sounds by event number, for an object (FUN_004bfa5c, EAX the object, EDX the event). Objects 1 and 2 are sounds with fixed parameters (pitch 1.0, volume 127, centre). 0x10000000 | car is a car; the other kinds are points the game owns.

KindWhat it does
0, 9, 10, 11Start the event's sound; 9, 10 and 11 then take the object's value onto the pitch, volume or pan: value × slope >> 16 + offset
2Stop event p1 of the object (its first sound and its engine groups)
3Stop all of the object's sounds
4Start engine group p1 for the object (20 at most)
7, 8Stop event p1 and fire event p2
12Set the echo environment to p1 (0 off)

A started sound goes into a list of 72 (object, channel, event) (0x695E10). Each frame FUN_004c00f0 walks it: sounds of objects 1 and 2 leave when they end; the others whose event has flag bit 3 get fresh parameters and an update, and leave when the update fails. Flag bit 5 makes stopping the event stop the sound. While the game is paused (0x6963C8) new sounds start at volume 0 and the frame does nothing.

Numbers are global: each bank's sounds, groups and events follow those of the banks loaded before it (FUN_004c1e48 keeps the three bases in the bank slot, +0x74, +0x78, +0x7C). FIX.MEG is loaded first by the sound start-up FUN_004bf9b0, so its numbers are the global ones; the 17 track banks follow in CIRCUITS.BIN order (FUN_0045ee70), then the cars' banks, which are empty. A bank's sounds play only once its play list is enabled (FUN_004c6518): FIX.MEG's at start-up, the track's and the cars' when a race starts (FUN_0045ef0c, FUN_0045ef44).

FIX.MEG's events and who fires them ​

EventKindSoundFired by
4, 31, 30, 29, 28, 320"Depart 1" … "Depart 5", "Depart GO"FUN_004815e8, the countdown: 0x1F, 0x1E, 0x1D, 0x1C, 4, then 0x20, object 1
364the engine groupFUN_0045ef9c at race start, for each car (car slot 0)
14, 15, 33 / 16, 17, 340 / 2brakes and their stopsthe car code (slots 3, 4)
3, 13, 350crashesthe collision code FUN_00425c50, FUN_00426184, FUN_00426ca4 (slots 7, 12, 5)
18, 190, 2sliding on the roofthe car code (slots 13, 14)
250repairFUN_0045ac84 (0x45aced), for the car
260the menu soundactivate_item (FUN_0043a344, 0x43a377): the item's sound word +0x58, object 1. Every item of the first edition's BETAMNU.BIN that has a sound has 26 (measured: 493 items; 10 have none)
20, 270, 2start and stop the CD sequence20: a track loudspeaker, fired by the track's "Depart" macro at the race's start and stopped by its "FinCourse" macro after the race ("Who starts and stops the music"). 27 is fired by nothing found: the music stops through event 20's stop flag
8, 2, 5, 9 .. 1212environment 0 to 58 (echo off) by the race loop and its pause and end paths (0x49d903, 0x49fac5, 0x49fb96, 0x4a0264); the others by track data (inferred)
0, 1, 6, 70, 2tyre squealthe car code

The cars' event numbers come from the car file: 16 slots read into the block at car +0x4F4 (docs/formats/cars.md). Every car on the disc has event 36 in slot 0. The physics step fires them by slot (observed by pod-physics, docs/systems/physics.md): 1 gear up, 2 gear down (none on the disc), 3 brake, 4 stop braking, 5 soft landing, 7 crash, 8 accelerate, 9 stop accelerating, 10 decelerate, 11 stop decelerating, 12 hard landing, 13 and 14 scrape start and stop. The car-to-car pass (0x42196c, pod-physics' cars::collisions) fires slot 7, the contact sound, for the first car of a pair whose impulse passes 0x2000, after the step's sounds; a wall impact is slot 7 too (observed by pod-physics). Car +0x444, the engine sound's value, is the driven wheels' spin times the gear ratio, clamped below the engine's maximum (FUN_00426184); the engine speed +0x440 follows it. The track banks' events are their ambient loops ("Amb Ville", "Amb Mine", "Amb Mer" for Beltane), fired by the race loop when the player's car enters a zone (FUN_0049dc98 with the 8-entry table at 0x57875C, filled from the track's data; observed). The track macros have three sound actions, registered by FUN_00486004 (pointers at 0x4862b9 …): 0x475a20 plays the item's event (its first word; with bit 15 set, relative to the current circuit's bank) for the macro's object, 0x475a9c the same at the camera (object 0x50000000), 0x475b18 at a point of the track (object 0x60000000 | point); 0x475a8c and 0x475b08 stop them.

POD's FIX.MEG has no event records 21 or 22, although the car files still point slots 8 and 10 at those numbers. FUN_00418010 emits the slots when engine sound state changes. The event dispatcher (FUN_004bfa5c) accepts these in-range holes as zero-filled event records, which start sound 0, the looped tyre squeal; the paired stop records 23 and 24 cannot stop it because the empty event has no stop flag (FUN_004bf17c). This is observed in the current bank and measured in docs/versions.md, “FIX.MEG's gaps”. Un-POD skips car sound slots without a corresponding event record, leaving the current engine group to provide the engine sound. Deviation: this avoids the original's repeated tyre squeal caused by the removed events.

Who starts and stops the music ​

The CD music is a sound of the track, not of the race code. Each track file holds a list of positioned records (.hp, haut-parleurs: loudspeakers; read_track_pointsFUN_0046d328, 0x38 bytes each at the current 0x58DA74): a word at +0 (0 for an unused slot), a point at +4 (3 × 16.16) and an event at +0x34 (i16; with bit 15 set, a number in the track's own bank, else a FIX.MEG number; -1 for none). Two macro actions use them (observed across the historical comparison; POD's are at 0x470934 and 0x4709dc, the same code):

  • "Son Localisé" (FUN_00475b18) takes the first word k of its macro instance and fires record k - 1's event for object 0x60000000 | the record's point (+4), unless the record is unused or its event is -1. A bank-relative event is added to the event base of the current circuit's bank (the bank slot's +8, through the circuit row's +0x88).
  • "Tue Sons Local" (FUN_00475bbc) calls FUN_004bf090 for the same object: every sound of that point stops, and a sound whose event has flag bit 5 stops its channel.

The race setup (FUN_0049f75c) runs the track's "Depart" macro after it starts the countdown, and FUN_0049fba4 runs "FinCourse" once the race loop has ended, before the end of race (FUN_00479114; docs/systems/race.md). On every POD track the records include event 20, the CD sequence, usually in record 1, next to the track bank's own loops. "Depart" plays each record once, and "FinCourse" stops each (measured with crates/pod-sound/tests/ speakers.rs on the 32 tracks: on 29 the "Depart" chain starts CD track 2 and the "FinCourse" chain stops it, through event 20's flag bit 5, the script stop FUN_004c5154 and the CD's). So the music starts with the countdown (as measured in the emulated PC, "The CD in the emulated PC") and stops when the race is left. Event 27 ("stop sequence") is never needed.

Three tracks have no loudspeaker action in "Depart": BABELWEB has no records, and FACTORY and ROAD70 have records but no such chain, so their races play no CD music (inferred from the data; measured for FACTORY: in race-loop's POD take, with the CD stream on its own sink from before the game started, the CD stream is digital silence for all 1,596 s, menus and 24 minutes of race included, while the game's own sound plays). Some records hold a plain number past FIX.MEG's events (Beltane's record 4: 27,770), which the dispatcher refuses with its "Bad number of event" message, so it plays nothing (observed in FUN_004bfa5c). MINE's "Arrivee" chain also stops its loudspeakers. The node of a chain names an action and an instance; the instance's data is the macro table's size bytes at instance × size (read_track_macros, FUN_0046cf5c; inferred: it makes every loudspeaker action name an existing record on every track).

The port: pod_sound::speakers reads the records and the two chains, Sound::speakers runs their actions (the object is 0x60000000 | the record's index: the address is not kept, a Deviation), and Un-POD runs "Depart" at the race's first pass and "FinCourse" when the race is left (apps/unpod/src/audio.rs).

The engine sound ​

An engine group (SMMH in the bank, 0xA4 bytes: a count and up to five layers of 0x20 bytes) is a set of looped recordings, each heard over a range of the engine's value (observed in FUN_004bfd2c, measured). POD's FIX.MEG has one group, Moteur unique, with one looped recording. Each frame, for each running group:

  1. The object's parameters from FUN_004bf5c8 with all four flags.
  2. v = pitch × (value × 0x418 >> 16 + 1.0) >> 16, with the value from FUN_0045f410: car +0x444 for the car's slots 0, 8 and 10.
  3. Each of its two voices whose layer range (fade in .. faded out) no longer holds v stops; the others get FUN_004bfc98: volume rising from fade in to full from, falling from full to to faded out, pitch v × slope >> 16 + offset.
  4. With a voice free and v above the first layer's start, the first layer with v below its end starts, and the one after it too once v is past its start.

FUN_0045f410 has a bug: for the car's slot 7 it computes a volume from car +0x4D4 and then stores the constant 6 over it (observed at 0x45f534). For slots 3 and 5 its value is dot(car+0x4A4, car+0x4C8) × 0x48002, scaled down by 2^8.

Music ​

CDAREF.TXT lists CD tracks 2 and 3; SCRIPT1.TXT plays CD entry 0 (track 2) for 9,999 loops (docs/formats/lists.md). FIX.MEG's record 7 ("sequence CD", kind 4) names Script1.txt, and event 20 plays it (observed):

  • FUN_004c50a8 stops the running script and plays the new one's current element: for a CD element it posts "PlayCD" with the track from CDAREF.
  • FUN_004c31e0 (the window procedure's MCI part) opens cdaudio at start-up with the time format TMSF, and plays a track from its start to its length (MCI_STATUS length, then MCI_PLAY from/to with notify).
  • On MCI's "successful" notice (MM_MCINOTIFY), FUN_004c51ec moves to the next element, or back to the first while loops remain, or stops the script.
  • FUN_004c5154 stops the script and the CD; the loop count is not restored, so a script restarted later has the loops it had left.

The music volume of Ubi.ini (MusicVolume) is the Windows mixer's CD line (FUN_004c0ae0), and SoundFxVolume the wave line (FUN_004c0ba8): they never touch the samples. Dolby calls FUN_004c74b8, which is empty: it changes nothing in this build. Stereo calls FUN_004c7464: a non-zero value swaps left and right (observed in FUN_0047816c).

The player's image (POD2_0.CUE) has one audio track, track 2, from 38:37:16; CDAREF.TXT's track 3 is not on it, and nothing in FIX.MEG plays it. cdda::TrackReader reads track 2's raw sectors from the image with disc-image.

The interface ​

rust
let mut sound = pod_sound::Sound::load(&files, &track_banks)?;   // FIX.MEG first, then the tracks
sound.enable_bank("BELTANE", &files)?;                            // at race start
sound.event(&world, pod_sound::events::PLAIN, 0x1F);              // FUN_004bfa5c
sound.frame(&world);                                              // FUN_004c00f0, after the steps
let block = sound.block();                                        // 220 frames, 22,050 Hz
for call in sound.take_cd() { /* Cd::Play(track) or Cd::Stop */ }
sound.track_ended();                                              // when the CD track ends

In Un-POD (apps/unpod/src/audio.rs, feature sound) one Sound serves the whole run, as the game starts it once. Per race the track's bank is enabled and, after each pass of the race loop: the sounds the car code fired in the steps (pod_game::Game::take_sounds: racer and slot, played as the car file's sound number for CAR | racer), the countdown's voices, then frame. Each car's CarSound comes from pod_game::Pose: engine (car +0x444, so the engine group follows the revs), sound_pos (+0x498) and vel (+0xC) when the source has them. A Cars on the physics fills those and Cars::take_sounds from pod-physics' Out.events.sounds; the replay has none, so its engines idle and the car code is silent (a Deviation: in the app). The CD music starts with the race (event 20) and plays from the disc image.

World gives the listener, each car's CarSound (position, velocity, sound slots, +0x444 and the two vectors) and any other object's position. The host calls block at the sample clock, 22,050 / 220 times a second. playback::Output (feature playback) plays the blocks and the CD track on cpal, natively and in the browser through Web Audio, with the two Ubi.ini volumes. examples/scene.rs plays the countdown and a car's engine.

In the native window, playback::Output's callback consumes the queued blocks and Audio::pump keeps that queue filled. Audio::advance_to is only for headless runs, where there is no device. It now returns immediately when a device is open; calling both paths had rendered and discarded blocks before the window queued audio, skipping initial blocks and desynchronizing playback at race start (observed in Session::pass and Audio::advance_to, 2026-10-25). This is host playback plumbing; the measured sound core is unchanged.

Compared with the running game ​

The emulated PC (run/vm) runs WINPOD.EXE under Wine with no sound card in QEMU. Inside the guest, PulseAudio with a null sink at the game's own format (22,050 Hz, 16-bit stereo) stands in for one: inside.sh sound, then inside.sh record FILE SECONDS reads the sink's monitor with parec. Wine opens the stream at 22,050 Hz, so PulseAudio does not resample it (observed: pactl list sink-inputs). The game then starts without its "no sound driver" question.

Measured (2026-10-24, measure/sound_recorded.py): the menu sound (FIX event 26, object 1, fired by Enter on the main menu) recorded from the game, against examples/fire.rs firing the same event in pod-sound:

LeftRight
Samples compared (55 windows of 220 frames)12,05412,054
Equal62.4%63.1%
Within ±193.6%93.7%
Within ±296.3%96.2%
Gain (recorded / port)1.000061.00006

So the level, the centre pan and the sound itself agree; a volume off by one step in 126 would show as a gain of 0.992. It is not a sample-for-sample check: the recording has gaps where the emulated PC did not feed the sound in time (recorded as silence, 50 of the 105 windows), and the samples that arrive carry ±1 noise (inferred: Wine's audio path converts the samples to floating point and back; the mixer code is the same one the emulator ran, and its FPU state is reset by fnsave). The sample-for-sample evidence stays the emulator's.

The CD in the emulated PC ​

QEMU's CD drive holds an ISO of the data track only, so in the emulated PC the game had no audio track to play. CDEmu gives it one: vhba, a virtual SCSI host built with DKMS, and cdemu-daemon, which loads the whole POD2_0.CUE into a drive (/dev/sr3) and plays its audio through PulseAudio (inside.sh cdemu, then POD_CD_DEV=/dev/sr3 inside.sh start). The drive reports both tracks: the data track and audio track 2 from 38:41:16 to the lead-out at 53:20:44, so the music is 14 min 39 s long. A play command from Linux comes out of the sink as track 2 (correlation 0.996, gain 0.996).

Measured (2026-10-24, first edition, Beltane, three runs):

  • In the intro and the menus no CD stream appears: the game plays no music there.
  • When a race starts, a second stream from the game appears (32-bit float, the game's own is 16-bit): Wine's MCI cdaudio plays the CD digitally, reading its sectors and playing them through DirectSound. Recorded on a sink of its own it is track 2 from the start (correlation 0.90 after two resamplings, 44.1 → 22.05 → 44.1 kHz).
  • On one timeline with the game's own sound: the music becomes audible about 4.7 s into the recording, and the countdown's "5" at 4.87 s. So the music starts with the countdown, from the top of track 2, within about 0.2 s (the two streams' buffers differ).
  • Started in a race, it plays on (the script loops it 9,999 times).
  • In the emulated PC the music stalls: it arrives in bursts of 0.3 to 0.7 s with silences between, at about 45% of real time. The bursts are consecutive pieces of track 2, nothing is skipped, and each matches the disc (correlation 1.00; the samples differ by up to about 100 from the two resamplings). This is the emulated PC's digital CD path (CDEmu's reads and Wine's playback loop under TCG), not the game's: the game only starts and stops MCI.

So event 20 is fired when a race starts. The menus' silence also means unpod-app's stand-in (event 20 when the menus come up) is not the original's.

A race ​

Measured (2026-10-24, Beltane, eight cars, the player in Gamma not driving): the sound of the start and the first 30 seconds, recorded as above while pod-physics' step log (run/vm/steplog.s) wrote every simulation step of the same race, camera included. measure/sound_race.py turns the log into the listener and the cars' sound fields, and examples/race.rs replays it through pod-sound: each car's engine (event 36) before the first step, a countdown voice whenever the countdown's number changes, frame after every step, blocks by the game time. examples/compare.rs compares the two:

Result
The six countdown voices, found in the recording by correlationall six, at delays of 1,436 to 1,482 ms after the replay (one constant, ±23 ms)
Their gain (recorded / replayed, least squares)0.738, 0.963, 0.955, 0.999, 0.965, 0.994
The engines' band spectra, recording against replaysimilarity 0.924 at the same moment, 0.849 against the replay 5 s later
The engines' spectral centroid over 20 scorrelation 0.58 at lag 0; at most 0.42 at the other lags of −3 .. 3 s

So the voices play when and as loud as in the game (the first is masked by the engines starting under it), and the engines change when the replay says they do: idle through the countdown, then the rise after GO. No window matches sample for sample. The engines are eight 3D sources replayed once per simulation step, where the game updates them once per frame (several steps), so their positions and pitches step differently; and the track's zone sounds, the brakes and crashes the car code fires, are not in the step log.

The first take had gaps throughout the race, up to half of the sound lost: the emulated PC does not keep Wine's default PulseAudio buffer filled. With a second of buffer (PULSE_LATENCY_MSEC=1000, now inside.sh start's default) the second take has none.

A POD race ​

Measured (2026-10-24, POD, Beltane, the player on the game's autopilot, Normal, no damage): race-loop's take of the game's sound with its step log (pod-beltane-sound, 8,374 passes; the log starts at the countdown's 3). measure/sound_race.py reads POD's addresses (camera 0x6218FC, player 0x6332F8, countdown 0x5A7600). The race's sound starts 67 s into the recording; until 126.5 s the CD music is mixed into the same stream (the CD stream had not been moved to its own sink yet), so the countdown and the first minute are heard under the music.

Result
The countdown voices 3, 2, 1 and GO, found by correlationdelays of 3,057, 3,386, 3,229 and 3,686 ms: spread over 630 ms
Their correlation and gain0.54, 0.59, 0.28, 0.43; gains 0.99, 1.01, 0.99, 1.25
The engines' spectral centroid, 900 sbest correlation 0.59 at −0.55 s; 0.54 at lag 0
The engines' band spectrasimilarity 0.589 at the same moment and 0.589 against 5 s later

So the voices are found at the replay's loudness, but not at one delay as in the first edition's take, and the engines' spectra say nothing about timing. Inferred: the step log has no wall time, and in the emulated PC the game's clock does not run at a constant rate against the audio stream, so placing blocks by the game time drifts; the music under the countdown lowers the correlations. Not yet compared: the effects the car code fires (the log does not hold them). Needed for a sample-level check: a wall time per pass in the step log.

Un-POD's live race ​

Measured (2026-10-24): unpod --live --headless on Beltane with the sound dump, the cars on pod-physics and their controllers (pod_game::live::Live, which fills Pose::engine, sound_pos, vel and Cars::take_sounds). The CD music plays from the countdown (the loudspeakers' "Depart"). The engines idle through the countdown and jump to a high, steady pitch at GO. POD's step log shows the same: car +0x444 is 0 before GO, then 40 to 65 million within a second, close to its clamp (65,535,999), for the rest of the race (pod-beltane-sound). Not checked here: which car-code sounds fire (the dump holds the mix only).

The local CD image ​

The app opens its CD source from --image FILE. For the default POD DATA folder (target/pod/disc/DATA), it also looks for target/pod/cd/POD2_0.CUE and uses it for CD music when present. This is the local disc image already used by the POD data setup; the app does not fetch or copy it. A custom --data DIR needs an explicit --image FILE for CD music. The CUE contains the audio track the CDAREF.TXT music script requests (track 2, started by the race's Depart chain). A headless sound dump's cd.raw is the direct check that samples were read from the disc and clocked into the output.

The MMX build ​

PODMMX.EXE's library is a newer version with a different mixer (docs/hardware/mmx.md): four frames a loop with pmulhw, the pan range 128, no pipelining, the overshoot kept when a loop wraps, a clamp on runaway steps, and five reverb and flanger environments where WINPOD has one echo. Its left word gets vol × (128 − pan), WINPOD's vol × pan. Measured (measure/sound_mmx.py, 300 single channels): on voices whose gain fits a signed word the two builds stay within about 1% (largest difference 203, 55% of samples within ±1); when vol × (128 − pan) or vol × pan exceeds 8,191 the MMX build's 16-bit gain wraps and that side comes out with the wrong sign and size. Both builds compute the same volumes (up to 126), so loud sounds panned off centre are wrong in PODMMX.EXE (inferred audible). The port follows WINPOD.

Open questions ​

  • A sample-for-sample comparison of a race: needs the frame boundaries in the step log (so frame runs when the game's FUN_004c00f0 does) and the events the car code fires. Wine's audio path adds ±1 noise either way.
  • Whether ROAD70 races without music too (FACTORY measured; the data says so).
  • The track macros' sound events (0x43a377: object 1, owner 0x1000) and the zone table 0x57875C: where their numbers come from.
  • The meaning of car +0x4A4, +0x4C8 (also the listener's velocity) and +0x4D4.
  • The kind-2 sounds (0x43) and the six flag words of a sound record (bits 3 to 7 of the table's flags): set on every record, read nowhere in the mixer.

Prose CC0 1.0, code Apache 2.0. POD: Planet of Death is © Ubi Soft; none of its files are on this site.