Skip to content

The sound engine, sample for sample ​

2026-10-24, the pod-sound session.

The countdown and a car's engine, mixed by pod-sound

Twelve seconds through pod-sound (crates/pod-sound/examples/scene.rs): the six countdown voices from FIX.MEG, then the Monza engine of a car that revs up after GO and drives past, left to right. Top: left side; bottom: right.

The banks were decoded last week, and the viewer could already play each sound. This session went after what the game does with them: how many play at once, how loud, from which side, at what pitch, and how the engine follows the car. The aim is a Rust crate that a port of the game can feed with the same events and get the same samples out.

What is in the game ​

The sound is Ubi Soft's own library, linked into WINPOD.EXE. Its assertion strings name its sources, srcson\pentium\sndplay.cxx and sound3d.cpp. It runs on a thread of its own and asks waveOut for 16-bit stereo at 22,050 Hz. It keeps 100 blocks of 880 bytes queued, about 10 ms each, and stays 25 blocks ahead of the one playing, so a sound starts about a quarter of a second after the game asks for it. There is no DirectSound. The music is not mixed at all: the CD drive plays the disc's audio track.

The layers, from the bottom:

  • a mixer of 64 channels, of which at most the ten loudest are heard;
  • the channel calls: start, change, stop, pause;
  • a 3D model that turns an object's position and speed into volume, pitch, pan and send;
  • events, read from the banks, which the game fires by number for an object;
  • engine groups, which play two of a set of recordings at once and fade between them with the engine's speed.

One multiply for two speakers ​

The mixer is hand-written x87 code. For each channel it keeps one double that holds both gains: the left side's times 2^32 plus the right side's. Each sample is multiplied by it and added to a large constant, and the stored double then carries both results in its mantissa: the right side in the low word, the left in bits 32 to 47. One multiply does the work of two.

The trick has edges. The x87 rounds the sum twice, once to its 80-bit register and once to the stored double. The port does the sum in 128-bit integers and rounds at both steps. Rounding once instead puts one sample of the measurement one step off. A negative value on the right side borrows from the left word, so a sound panned fully right still puts −1 into the left side. And the loop is pipelined: it multiplies one sample ahead, and the one in flight when the block fills or a loop restarts is never heard.

The emulator runs the routine itself. measure/sound_mixer.py builds the channel table in memory with random sounds, positions, pitches, loops, volumes and pans, runs FUN_004df88a one to four times, and writes every block. The Rust mixer gives the same 215,044 frames.

Ten channels, and the pitch glide ​

Before each block the library marks the ten channels that will be mixed. It takes the first ten loud enough. A louder one found after that replaces the first chosen one that is quieter than it, not the quietest. When the game changes a sound's pitch, the change is spread over as many blocks as passed between its last eight changes. The code keeps those eight counts in a small array and never wraps its index: on the eighth step the store meant for a ninth entry lands on the index itself and puts it back to zero. A test that masks the index instead fails on case 139 of 300.

These calls take the library's mutex through WaitForSingleObject and ReleaseMutex. Under the emulator those two imports are stubs, and the calls run in random sequences against the game's own channel table. 18,744 calls, and every block between them, match.

Where a sound is heard from ​

The three layers of the Monza engine against the engine's value

FIX.MEG's Monza group: the volume of each recording against the value v that the engine speed sets, as FUN_004bfc98 computes it in pod-sound. The second layer stays silent below v ≈ 1.2, where its own pitch formula goes below 1.

The volume falls with distance as 0x7F0 / (d + 16). The distance is not a true length but the octagonal estimate the game uses everywhere: the larger of two axes plus half the smaller. The pitch has a Doppler shift with sound at 300 units a second. The pan comes from the camera's side axis, in the horizontal plane only. The length the Doppler needs comes from a square-root table the game fills at start-up. 5,000 random cars and listeners give the same four numbers in the port as in FUN_004bf5c8.

The engine ​

Every car on the disc has the same engine sound: event 36, "Moteur Monza", in the first of its sound slots. It starts an engine group, three looped recordings each heard over a range of the car's value at +0x444. Each frame the group stops a voice whose range no longer holds the value, sets the volume and pitch of the others, and starts the next layer when one is free. So the idle recording hands over to the second as the revs rise, and the second to the third.

To check this, measure/sound_events.py loads FIX.MEG's real events, groups and samples into the emulator, moves up to three cars, sweeps their engines and fires random events at them. In 150 cases the lists of playing sounds, the engine voices and the 3,943 blocks are the same in the port.

The music, and the MMX build ​

FIX.MEG's "sequence CD" record names Script1.txt, which plays CD track 2 for 9,999 loops. MCI plays the whole track and reports its end, and the script starts it again. The player's disc image has one audio track, track 2. CDAREF.TXT also names a track 3, which is not on it. pod-sound reads the track's raw sectors from the image and plays them beside the effects.

PODMMX.EXE has a newer version of the library with an MMX mixer. Run side by side on the same channels, it stays within about 1% of WINPOD while a voice's gain fits a signed word. Beyond that its 16-bit gain wraps, and loud sounds panned off centre come out with the wrong sign. WINPOD is the reference.

What is next ​

  • The emulated PC has no sound card, so nothing yet compares the port with the running game's output. A sb16 or intel-hda device in run/vm would let it record.
  • The triggers live in code other sessions are porting: the countdown in the race loop, the car and crash sounds in the physics, the menu sounds and the CD sequence in the front end. The interface for them is in docs/systems/sound.md.

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