How the game plays its sounds through the Miles Sound System (MSS32.DLL): the banks' sounds on voices of their own, the effects placed in 3D around the camera, and the music. OpenReliant's code is game/hog_snd.zig and game/sound3d.zig; what stands in for Miles is in Sound in OpenReliant. The banks are .fat files.
WinMain calls sound_init (0x00481440) with 10 voices. It starts Miles, opens a digital driver at 22,050 Hz in 16-bit stereo, or at 11,025 Hz where that fails (sound_driver_open, 0x00482D10), allocates a sample for each voice, and starts the 100 Hz timer, tick_timer (0x004827C0). Asked for them, it also opens the CD's audio and sets aside two more samples and a double buffer for the radio's speech. sound_3d_providers (0x004817E0) lists Miles's 3D providers and sound_3d_open (0x00481900) opens the one the settings name, else the first of Miles Fast 2D Positional Audio, Aureal A3D Interactive (TM), Creative Labs EAX (TM) and RAD Game Tools RSX 3D Audio, with Dolby Surround for one of them where asked. It allocates a 3D voice for each sample the provider supports, up to 64, and sets the effects up on them (sound3d_init). That sets the provider's room to EAX's generic one (AIL_set_3D_room_type, 0), with a damping of 1 and an effect volume and decay time of 0, and each 3D sound it plays gets an effects level of 0, with no obstruction or occlusion: the room's reverb is never heard.
As the game's window is put away (window_suspended, 0x005DDD28, which 0x004A8260 sets and input_init clears) while the renderer runs (app_active, 0x005D6CAC), the message pump (message_pump, 0x004AAB20) pauses the music, the 3D voices and the voices, where no video is playing, and waits on the window's messages; once the window is back, it resumes the sound. app_inactive_paused (0x005D6CAD) remembers that it paused. Only in a multiplayer session with a mission loaded does it pause the mission too (game_pause), which then stays in its pause menu. In single player the mission is not paused, and the timer's ticks go on while the pump waits. Unverified: that the mission runs the ticks it missed once the window is active again.
Four settings, each from 0 to 127, in [Sound] of starlancer.ini, which the options screen resets
to their defaults:
| Key | Address | Default | Scales |
|---|---|---|---|
Mastervolume |
0x005D5A70 |
127 | everything |
Fxvolume |
0x005D5A74 |
80 | the banks' sounds and the 3D sounds |
Musicvolume |
0x005D55EC |
80 | the music |
Speechvolume |
0x005D5E88 |
127 | the radio's speech |
sound_volumes_apply (0x00482990) sets them again on the music and every voice playing when one
changes.
A voice of sound_voices (0x00565080) is 0x20 bytes:
| Offset | Field |
|---|---|
0x00 |
Miles's sample |
0x04 |
Set while the display holds the voice (hud_draw) |
0x08 |
The playing sound's priority, from its bank entry |
0x10 |
Set while it fades out |
0x14 |
What each step of the fade takes off its volume |
0x18 |
The sound's own rate |
0x1C |
Its volume as asked, 0 to 127 |
sound_play (0x00481F80) plays sound n of a bank at a volume, a loop count, a pan and a pitch.
It takes the first voice past voice 0 that has finished; else the first that was stopped; else the
voice of lowest priority not held by the display, which it ends, if that priority is below the new
sound's. sound_play_on_voice (0x004820C0) plays on a given voice. sound_start (0x004826A0)
hands Miles the WAVE file at the entry's offset in the bank, at its own rate times 2^(n/24) for a
pitch of n quarter tones (0x00481400, clamped to 96 each way), and at the volume
round(((Fxvolume × volume) / 128) × Mastervolume / 127).
Improvement: OpenReliant divides by 127 where the game multiplies by a rounded reciprocal, for
the master volume's share (0x004DC6B0) and the engine's volume (0x004DC9C8), and divides a 3D
sound's length by the bytes a tick plays where the game multiplies by their reciprocal
(0x004DC8B8).
Every five ticks tick_timer steps the fades: a fading voice loses its step of volume and ends at
nothing. sound_voice_fade (0x004824C0) and sound_fade_all (0x00482510) start them;
sound_pause_all (0x004825D0) and sound_resume_all (0x00482630) stop the playing voices and
start them again.
The timer is a multimedia timer of its own (timer_start, 0x004A70F0), so the fades continue
while a movie plays: the menu music, which starts fading out when a campaign starts, is gone half a
second into the new pilot's intro. OpenReliant runs the timer once a frame from the rooms' loop,
the movies' loop and the wait before a launch (hog_snd.Sound.runTimer), and a mission runs it with
its own clock (hog_snd.tickTimer, hog_snd.Sound.timerTick).
sound_buffer_at (0x00482160) gathers a sound of the first 18 slots at a place in the world: its
level in each ear from its distance and which side of the camera it lies. Once a frame
sound_buffers_play (0x004822F0) plays each slot gathered as sound n of bank_stdsmp, panned
by its two levels and as loud as the louder, then clears them.
Each effect is a definition of the table at 0x00507140, 0x44 bytes each, closed by a record
whose entry is -1; make sound-tables derives
sound3d/sounds.zig from it.
| Offset | Field |
|---|---|
0x00 |
The sound in smp3d.fat |
0x04 |
Its share of the effects volume |
0x08 |
Loop count, 0 for ever |
0x0C |
What it follows (below) |
0x10 |
Minimum distance: heard at full volume within it |
0x14 |
Maximum distance: not started, and ended, beyond it |
0x18, 0x1C |
The cone's inner and outer angles, in degrees |
0x20 |
The volume outside the outer cone, of 127 |
0x24 |
Its name, such as GUN01, EXPLOSION01, PSHIP01 |
What a sound follows sets its place as it starts and each frame after:
| Value | Follows |
|---|---|
| 0 | A shot, by its record in the bullet pool: where it is, and its heading |
| 1 | A point and a direction given as it starts |
| 2 | A point given as it starts |
| 3 | A missile, by its record at 0x005887F0 |
| 4 | An object, by its slot; the player's own 200 units from its ship |
A 3D voice is 0x44 bytes at 0x00563F60: Miles's 3D sample at 0x00, what it follows at
0x04, its owner at 0x08 (-1 while free), the priority at 0x10, the maximum distance at 0x14,
the point and direction at 0x18 and 0x24, whether it was borrowed at 0x34, the sound at
0x38, the frame it started at 0x3C, and the sound decompressed to PCM at 0x40.
Each 3D voice has a class (0x0058CB1C), from one of three rows at 0x005085B4 by how many voices
the provider has: up to 14, up to 30, or more. The names are the executable's own (0x00508614):
| Class | Name |
|---|---|
| 0 | not reserved |
| 1 | player guns |
| 2 | player fx |
| 3 | explosions |
| 4 | guaranteed |
| 5 | player engines |
| 6 | player burners |
| 7 | flyby |
A sound of classes 5 and 6 plays on the one voice of its class. Any other takes a voice of its own class that is free or borrowed, else such a voice of class 0, else borrows a free voice of any class but 4, 5 and 6.
sound3d_play (0x0049D360) takes a place and a direction, an owner, the sound, a volume and a
class; a further argument goes unread. It places the sound by what it follows, relative to the
camera and scaled by 0.0004 into Miles's units, its velocity by 1e-5 into a millisecond's. A
sound beyond its maximum distance is not started, but for the engines' (PSHIP01 to PSHIP12) and
BURNER01. It decompresses the bank's ADPCM into PCM (AIL_decompress_ADPCM), since Miles's 3D
samples play PCM only, and plays it at the volume Mastervolume / 127 × Fxvolume × share × volume,
with the definition's loop count, cone and distances, at 22,050 Hz; the two explosions at
18050 + 7000 × rand() / 32767. Miles's y points up where the camera's points down, so every
place, direction and velocity goes over with y negated.
Improvement: the explosions' rate takes the sound's own random numbers
(hog_snd.Sound.random). The game draws it from its own, but only for a sound that plays, so a
game that hears nothing would go on from other numbers. So the effects come out the same whether
the sound plays or not (Random numbers).
Fix: a sound that follows a shot, a missile or an object is placed by the owner's record, which the game reads for an owner of -1 too, before the table. OpenReliant plays nothing without an owner, or with one past its table.
Once a frame sound_3d_update (0x00481BF0) runs the engine's sound, then each voice playing. A
voice past its sound's length, length / 441 ticks of 16-bit sound at 22,050 Hz, is freed but for
the engine's and the afterburner's. A shot's and a missile's stay where they started; the rest are
placed again by what they follow, and freed once beyond their maximum distance or once their object
has gone. Nothing gives an object its voice (+0xB96 stays 0xFFFF), so missile_end, which ends
a missile's, never does.
Improvement: a missile's voice follows it as an object's does, and is its own, so it ends with
it; it keeps its full volume 1.5 times as far (sound3d.MissileSound.follows). The game would keep
the voice in the missile's object (+0xB96). OpenReliant's sound keeps it itself, by the missile's
record and the record's reuse count (hog_snd.Sound.missile_voices), so that the sound writes
nothing into the game's objects.
Improvement: the game's rates are made for its 3D sounds, which are recorded at 22,050 Hz, so a
mod's sound at another rate would play at another pitch: an octave low at 44,100 Hz. OpenReliant
scales the rates a mod's sound plays at by the sound's own rate divided by 22,050
(sound3d.rateScale), whether it replaces one of the bank's sounds or belongs to a ship type or a
gun. The game's own sounds play at the game's rates, including CLOAK01, which is recorded at
44,100 Hz and plays an octave low. A voice is freed once its sound has finished at the rate it
plays at: its frames divided by 22,050, scaled for a mod's sound, so a mod's sound at another rate
or in stereo isn't cut short or kept too long (hog_snd.Sound.update3D).
sound3d_engine_sound (0x0049DCB0) picks the engine's sound for the player's ship type:
PSHIP01 on, by type; types from 244 count again from 0. The game starts it in three places: the
Reliant's and the Yamato's launches (launch_reliant_run at 0x0041B2AE, launch_yamato_run at
0x00419A50), and the effect of a multiplayer respawn (order 121, 0x004B0DF2). It starts it
again as the afterburner ends where the afterburner has no voice of its own (0x0049DE94).
sound3d_engine_update (0x0049DCF0) does nothing while the engine has no voice, and otherwise
keeps it going through three states (0x0058CB04):
- Idle: the engine's rate and volume by the ship type's row of the tables at
0x00508740,0x00508774,0x005087A8and0x005087DC, at no throttle plus the throttle's share of what full throttle adds. - Burning, while the afterburner or reverse thrust is on:
BURNER01on the afterburner's voice, its volume growing from 5 by 0.8 a tick to 70, and its rate12000 + 90 × that, or 7000 in reverse. - Cooling, once let go: the afterburner's sound fades over 25 ticks while the engine's plays on.
In view 13 neither is heard.
Both sounds loop until they're ended. As a mission ends, mission_end (0x004942B0) ends the 2D
sounds, then the music, then every 3D sound (sound_3d_end_all, 0x00481BA0, called at
0x0049436B), so the engine stops with the mission. OpenReliant does the same as it lets the
mission go.
Fix: a player whose ship starts in space, or launches from any other carrier, flies the whole
mission without the engine's sound or the afterburner's, since the update does nothing until the
engine has a voice. The campaign launches the player from the Reliant or the Yamato, but missions 81
to 85 and 87, the multiplayer maps, and mission 99 start the player's ship in space, and a custom
mission can do either. OpenReliant starts the engine as such a ship comes into flight: as the
mission starts for a ship with no launch, and as the ship lets go of any carrier but the Reliant and
the Yamato, whose launches start it themselves (sound3d.hearEngine). Every start plays a mod's
ship type's own engine sound where it has one (EngineSound,
Modding).
The same update hears each fighter flying past the camera: within 10,000 units, not disabled,
hidden or exploding, at a throttle of 0.4 or more and moving at 100 or more, its velocity at least
a right angle from where the camera looks, or for a hostile one at least 41 degrees. A hostile
ship sounds PASS01, another PASS02, on a voice of class 7, no more than once in 500 ticks, or
200 for the player's own, which is not heard in views 0 to 3, 12 and 15. The game keeps the frame it
was heard in the object (+0x67C); OpenReliant's sound keeps it itself, by the object's slot and
the slot's reuse count (hog_snd.Sound.flybys).
music_play (0x00482A80) plays a file as a Miles stream at a level and a loop count, at once
where its now is 1 (0x00482A9D), or for any other value once the music playing has faded out
(hog_snd.Sound.When.of). The mission script's PlayMusic (cmd_PlayMusic) plays music\ and
the name it is given, for ever, at level 80, its second argument the now. A piece the table at 0x005017A0 names
loops back to its own point, a byte offset into its data, once it has played through; the rest from
the start. The stream's volume is round(((Musicvolume × level) / 127) × Mastervolume / 127).
music_play fades the music playing out with music_fade_out (0x00482960), which takes the step
it is given, 5 here, off the level every five ticks from the tick it starts on, until the stream
closes, and music_update (0x00482C30) then starts the piece waiting. WinMain fades the music
out by 15 before the hangar's movie of a mission it flies (Movies). Mission 1 plays new_launch.wav as its
wing launches, and new_searching mission 09.wav once it is out. The CD player in the rooms plays
the pieces of its lists, and turns their level (The CD player).
Added by OpenReliant: player and menu scripts play sounds on the game's voices
(Sound). Each script sound gets an ID, which its voice stores
(hog_snd.Starter). A game sound that takes the voice clears the ID.
- 2D sounds (
hog_snd.Sound.playFile) pick a voice assound_playdoes, at priority 1, the lowest priority in the banks. A script sound never takes a voice from a game sound, and a game sound with a higher priority takes a script sound's voice when no voice is free. A standard game sound played by a script keeps its bank priority. - 3D sounds (
sound3d.playOwn) pick a voice assound3d_playdoes: an unreserved voice, or else a free voice of another class. They have no cone, and play at their own rate times their pitch. A sound at a point keeps its voice until it ends (Voice3D.script_owner), unlike a game sound at a point, and its position is updated every frame. - A 3D script sound doesn't start beyond its maximum distance.
sound_3d_updatefrees its voice when the sound ends, not after a fixed length, so a looping sound plays until it's stopped. - Toward its maximum distance, a 3D script sound fades out instead of cutting off. Beyond the
minimum distance, its gain is the lower of the inverse distance law and
(1 - (distance - minimum) / (maximum - minimum))squared (sound3d.fadeToFar), which FMOD calls the inverse tapered roll-off. Beyond the maximum distance it is silent but keeps playing, and becomes audible again when it comes back in range. hog_snd.Sound.stopMusicstops the music and drops any queued piece, asmusic_playdoes before it starts a piece at once.hog_snd.Sound.pieceOnreturns the piece playing.
sound_init allocates a sample for the radio's speech (speech_sample, 0x00563F18) with two
buffers (0x00563F20), which cbox.cpp streams a line through: speech_start (0x00461EB0)
sets the sample up as 16-bit stereo at 22,050 Hz, both channels the same, at
round(Speechvolume × Mastervolume / 127), which sound_volumes_apply sets again, and a timer
decodes the line into the buffers as it plays (Speech files). OpenReliant
decodes a line whole as it starts and plays it on the sample
(cbox.zig), which then goes through OpenAL Soft's resampling
and the master bus like every sound (Sound).
As a mission ends, mission_end stops the speech after the 3D sounds (speech_stop_all,
0x004620D0, called at 0x00494370): it ends the eight streams and the speech sample, so a line
still playing stops with the mission. OpenReliant stops the radio's line there too
(radio.Radio.stopSpeech), and the next mission's start empties the radio's queue (radio_reset).
Improvement: the recordings push past full scale in a few samples of every thousand, which the
game cuts flat, and crackle; OpenReliant rounds the peaks off, unchanged within 0.8 of full scale
and eased toward it past that by a hyperbolic tangent (cbox.softClip). Improvement: the lines
ring in the cockpit's cabin, as Betty's warnings do, where the game plays them dry. --original
cuts the peaks and plays the lines dry.
Improvement: without a speech sample, as with --no-sound, a line still lasts as long as it
would play, timed by the game's clock (game_ticks), which stops while the game is paused
(cbox.Player). A line paused keeps the time it had left. Whatever waits for a line then waits as
long as with sound: a scene runs its length in the rooms, the induction, the briefing and the
loadout. Otherwise each line would end as soon as it started. The radio times its lines by the
game's clock with sound too (The radio).
The crew in the rooms speak their lines on voice 2, an MP3 file each, which Miles's MP3 decoder
(MP3DEC.ASI) plays (The crew). OpenReliant reads the frames
(MP3 files), and FFmpeg's MP3 decoder decodes them.
In OpenReliant the game frames ask for their sounds rather than play them
(hog_snd.commands.Commands, gameobj.World.sounds). Each pass, the main thread plays what the game
frames asked for since the pass before, in the order they asked, and it plays them right after a
mission's start. So the game frames never touch the sound, which plays on the main thread.
- 3D sounds. The game frames place a sound by what it follows in their own objects as they ask
for it (
sound3d.place), so it starts where it would have started then. The main thread checks its range from where the camera stands, picks its voice and starts it (sound3d.startEffect). - Kept voices. The voices the game keeps to end later, the missile lock's tone, the scanner's
beeping and Betty's missile names, are named by what keeps them (
commands.Kept). Ending one ends whatever plays on its voice by then, as ending a voice by its number does in the game. Where no voice is free, the lock's tone and the scanner's loop wait for one, as the game asks for them again until they play. - Speech and music. The radio's lines go the same way, each with its file as the archive holds
it, which the main thread unscrambles as it plays the line on the speech sample
(Speech). So does the script's music (
PlayMusic). - Timers. What the sound kept for the game moves to the player: when the armour last warned
and when a shot last sounded on the player's hull (
input.Player.armor_warned_at,hull_heard_at).
- A shot the step hears (
guns.heard) plays its gun type's sound following it, on a voice of the player's guns for the player's shots, of the guaranteed ones for the Huge Guns'. - A flak shell plays
FLAK01as it bursts. - The player's ship warns, sound 1 of
betty.fat, once a quadrant has lost its shield and half its armour (Objects). - A Huge Gun's shot striking a component plays
EXPLOSION01where it strikes. - A shot through to a hull plays
ARMOUR01where it struck, facing the camera, orPLAYERHITon the player's ship, following it, at most every 30 ticks (shieldfx_create,0x004A0310). The game playsARMOUR01at the point in the part's own frame, taken for one in the world. Improvement: OpenReliant plays it where the shot struck. - A shield generator whose part
node_drawfinds destroyed playsSHLDDOWNat the part, facing its way, and its object loses flag0x4000. - Turning the missile ring plays
MISSILESELECTat the player's ship (hud_target_keys).
The display's own sounds are in The display's sounds.
Not ported: the CD's audio.