Skip to content

Latest commit

 

History

History
323 lines (263 loc) · 21.1 KB

sound.md

File metadata and controls

323 lines (263 loc) · 21.1 KB

Sound

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.

Start-up

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.

Volumes

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.

The banks' sounds

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).

Positional sounds of a frame

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.

3D sounds

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.

Voice classes

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.

Playing

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).

The player's engine

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, 0x005087A8 and 0x005087DC, at no throttle plus the throttle's share of what full throttle adds.
  • Burning, while the afterburner or reverse thrust is on: BURNER01 on the afterburner's voice, its volume growing from 5 by 0.8 a tick to 70, and its rate 12000 + 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).

Ships flying past

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

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).

Script sounds

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 as sound_play does, 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 as sound3d_play does: 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_update frees 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.stopMusic stops the music and drops any queued piece, as music_play does before it starts a piece at once. hog_snd.Sound.pieceOn returns the piece playing.

Speech

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.

The game frames' sounds

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).

Where the sounds come from

  • 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 FLAK01 as 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 EXPLOSION01 where it strikes.
  • A shot through to a hull plays ARMOUR01 where it struck, facing the camera, or PLAYERHIT on the player's ship, following it, at most every 30 ticks (shieldfx_create, 0x004A0310). The game plays ARMOUR01 at 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_draw finds destroyed plays SHLDDOWN at the part, facing its way, and its object loses flag 0x4000.
  • Turning the missile ring plays MISSILESELECT at the player's ship (hud_target_keys).

The display's own sounds are in The display's sounds.

Not ported: the CD's audio.