Skip to content

Sound groups, ambients and impulses (OSBD) ​

What the game plays is described in three kinds of sound object, all in level 0's .raw:

  • Groups (.grp): a list of permutations, each a sound (SNDD) with a weight and a volume and pitch range. Playing a group picks one permutation at random.
  • Ambients (.amb): up to five groups that make a longer sound: an in sound, two base tracks that loop, an out sound, and details played at random times around it. Music and dialog are ambients.
  • Impulses (.imp): a group played once at a position, with its distances and an alternate impulse for when too many of them play.

Each one is an OSBD instance (instance files) that points at a chunk of the .raw. The instance's name gives the object's: OSBDmus_chase.amb is the ambient mus_chase. The 2,718 instances are all in level 0: 1,415 groups, 1,005 ambients, 298 impulses.

OpenOni reads them with src/sound.c, which also plays them (below). Worked out from Oni.exe v1.0: OSiBinaryData_ProcHandler 0x5147b0 and the three class loaders it calls. Field roles are confirmed where the playing code uses them; the rest is marked.

The instance ​

OffsetWhat
0x00Size of the chunk
0x04Its offset in the .raw

OSBD's callback (0x5147b0, registered by OSiScriptCommands_Initialize 0x512cb0 through 0x423e70) adds the .raw's base to 0x04 when the instance loads. It then takes the instance's name without the tag, cuts it at the extension (0x426690), and calls the loader the chunk's class asks for. The loader replaces spaces by _ and keeps at most 31 characters. An object of a name that exists already is reloaded in place.

The chunk ​

All values are little-endian. The class is four characters, stored reversed as in other Bungie binary data.

OffsetWhat
0x00Class: mASO (OSAm, ambient), rGSO (OSGr, group), mISO (OSIm, impulse)
0x04Size of what follows
0x08Version
0x0cThe class's data

Group (OSGr, 0x513950) ​

Versions on the CD: 4 and 6. The fields after the version depend on it:

Present fromField
version 2Volume, float (else 1)confirmed: multiplies the channel's volume
version 3Pitch, float (else 1)confirmed: multiplies the permutation's
version 6Flags, u16; bit 0: don't repeat the last permutationconfirmed
version 6Last permutation played, u16confirmed (the loader keeps the stored value)
allChannels, u32: 1 mono, 2 stereoconfirmed: which channels an ambient takes for it
allPermutation count, u32confirmed
allThe permutations

Before version 6 there is no flags field, and a group of more than three permutations gets the don't-repeat flag.

A permutation is 0x34 bytes:

OffsetWhat
0x00Weight, u32
0x04Minimum and maximum volume, floats
0x0cMinimum and maximum pitch, floats
0x14The sound's name, 32 bytes: the SNDD instance's name (ac5lp1.aif)

In memory a group is a 0x34-byte header: name, volume, pitch, the two u16s, channels, and an array of 0x38-byte permutations. The permutation in memory has the SNDD pointer at 0x14 and its name at 0x18.

Ambient (OSAm, 0x513580) ​

Versions on the CD: 4, 5 and 6.

OffsetWhat
0x0cPriority, s32confirmed: the channel priority it asks for
0x10Flags (below)confirmed
0x14Detail range, float: how far in x and in z a detail plays from the ambientconfirmed
0x18Detail interval, minimum and maximum, floats, secondsconfirmed
0x20Distances, minimum and maximum, floats: full volume up to the first, silent from the secondconfirmed
0x28The detail group's name, 32 bytesconfirmed
0x48Base track 1's groupconfirmed
0x68Base track 2's groupconfirmed
0x88The in sound's groupconfirmed
0xa8The out sound's groupconfirmed
0xc8Version 5 on: how many may play at once (else 3)confirmed
0xccVersion 6 on: minimum occlusion, float (else 0)inferred from the Mac build's names; unused here

Flags:

BitWhat
0Interrupt on stop: a stop cuts the channels at once. Otherwise the base track plays to its end, then the out sound
1Play once: the base tracks don't loop
2Can pan: its channels pan with the position

Group names are resolved at level load (0x482670) with a case-sensitive search; a missing one is no group. In memory the ambient is 0xfc bytes: the fields from 0x20, the five group pointers at 0x44 (detail, base 1, base 2, in, out), the five names at 0x58, the subtitle cache at 0xf8.

On the CD a dialog line is an ambient with priority 3, flags 3 and base track 1 only (c00_01_74shinatama plays the group of the same name, whose one sound is c00_01_74shinatama.aif). Music has an in, a base and an out track (mus_chase: mus_chase_in, mus_chase with eight segments, mus_chase_out).

Impulse (OSIm, 0x513d90) ​

Versions on the CD: 3, 4, 5 and 6.

OffsetWhat
0x0cThe group's name, 32 bytesconfirmed
0x2cPriority, s32confirmed
0x30Distances, minimum and maximum, floatsconfirmed
0x38Maximum and minimum volume angle, minimum angle attenuation, floatsinferred (a cone); unused by the playing code
0x44Version 4 on: alternate threshold, u32confirmed
0x48Version 4 on: the alternate impulse's name, 32 bytesconfirmed
0x68Version 5 on: impact velocity, floatinferred
0x6cVersion 6 on: minimum occlusion, floatinferred

The offsets from 0x68 hold for version 4 and on; a version 3 impulse ends at 0x44.

How they play ​

Channels ​

The sound system has 16 channels (0x485be0), 7 stereo and 9 mono (0x4830c0: 16 × 0.4, rounded). Each is a DirectSound buffer of 22,050 Hz, 16 bits, streamed from a thread. A sound plays on a channel of its own kind. Its volume is the product of four parts: the ambient's or the impulse's, the distance's, the permutation's random one, and the overall volume. That product is times the group's volume. It goes to the buffer (0x485480) in hundredths of a decibel:

  • Above 0.05: round(3333 × v) − 3333, so 1 is full volume and 0.5 is −16.7 dB.
  • At 0.05 or less: −10,000, silence.
  • Above 1 the value is out of DirectSound's range (inferred: the call fails and the volume stays).

The pitch becomes the buffer's frequency, 22,050 × pitch Hz (0x485460). So a 44.1 kHz sound plays at half speed: the ACM's PCM keeps the source rate and the buffer doesn't (inferred from the code, not heard on Windows). The pan (0x4853b0) is ±(3333 − round(3333 × v)) with v from 0.5 (to the side) to 1 (ahead or behind). Only channels that may pan get it, and only for sounds more than 7 units away.

A channel's flags (byte 0x1b): playing, looping, locked (held by an ambient), updating (its sound just ended), paused, can pan, owned by an ambient. Every channel has priority 1, and nothing writes it again.

SSiSoundChannels_GetChannelForPlay 0x47fab0 finds a channel for a sound of a priority:

  1. The first one neither locked nor playing.
  2. Else the first unlocked one of a lower priority. With priority 3 (scripts' ambients and dialogs), that is any unlocked one.
  3. Else the first unlocked one of priority up to the asked one and volume 0.1 or less.
  4. Else the quietest one of priority up to the asked one, under full volume, locked or not. It is taken from its ambient if it has one.

Priority 0 (ambient details) only gets a free channel.

Groups ​

SSiGroup_SelectPermutation 0x480040 draws with the second random generator (0x426e30): r = (draw × total weight) >> 16, the first permutation whose running sum reaches r. A don't-repeat group draws again if it got the last one. The flag that ends the retries is never set back, so once a repeat happens all 10 draws are made and the tenth is kept, repeat or not.

SSrPermutation_Play 0x47f8d0 draws the volume, then the pitch, with UUrRandom 0x426e10, between the permutation's minimum and maximum. It resolves the sound by name if needed. A looping channel plays the group again when its sound ends: each loop is a new permutation (the stream's fill, 0x484ef0, calls SSrGroup_Play 0x47ff50 on the channel).

Ambients ​

SSrAmbient_Start 0x4821f0 takes one of 16 slots. It fails if the ambient already plays as often as it may. Then it runs the slot's state machine (0x480c20), which also runs every frame and whenever one of its channels' sound ends:

StateWhat happens
startA channel for base 1 (or the in, or the out sound's group) and one for base 2, at the ambient's priority, locked. If none: details only, or done
inThe in sound on the first channel
in waitUntil it ends
baseThe base tracks, looping unless play once; the first detail is timed
bodyBase tracks play; a lost channel is taken again (at volume 0); details at their times
end waitLoops off; until the tracks end
outThe out sound on the first channel; the second is let go
out waitUntil it ends
releaseThe channels are let go, the slot is free

SSrAmbient_Stop 0x4823c0 turns the loops off and goes to end wait, so music ends its current segment and then plays its out track. A channel taken again in the body state is set to volume 0. Then the code asks for a half-second fade to the ambient's volume, but the fade only starts when the target changes, so the channel stays silent until the volume is set again. The port keeps this.

Volumes fade (0x482000, 0x481860) by a rate per sixtieth of a second toward the target. The overall volume slides to its target by at most 0.05 a frame (0x483bd0).

Music, dialog and the scripts ​

The script commands (registered by 0x512cb0) use these lookups:

  • sound_music_* and sound_ambient_* are the same functions (0x514410, 0x514500, 0x514550). They start the ambient at priority 3 in one of 16 script slots, and stop or fade it by name.
  • sound_dialog_play (0x514140) starts the ambient as the dialog. There is one dialog at a time; while one plays, the call does nothing.
  • sound_dialog_play_block (0x5142e0) waits (the thread stalls) while a dialog plays, then starts its own and goes on.
  • sound_dialog_play_interrupt (0x514300) halts the current dialog first.

A dialog counts as playing until its ambient ends, or sooner when the first sound of base track 1 has played its length. It counts for 2 s if the ambient couldn't start. The scripts' sound_dialog_play_block pause (56 times in level 1) names no ambient, so it only waits for the current dialog. sound_impulse_play (0x5145f0) plays the impulse at the listener.

The placed ambients and the walls ​

The speech and the particles' sounds start through the OS layer (OSrAmbient_Start 0x511730), not the sound system directly: a table of 1024 records of 0x4c bytes (+0x08 the ambient, +0x0c the sound system's id or -1, +0x10 placed, +0x14 the place, +0x20 and +0x24 the distances, the ambient's own when not given, +0x28 the volume, +0x2c the record's index mod 12, +0x30 a time, +0x34 a cached volume, 1 at first), its ids the slots of an array at 0x61fa34.

The listener (0x61fa18, the same as the sound system's, set by OSrUpdate 0x514f20 from iComputeDeltaTicks 0x4fafb0): in the follow camera with a player, her ears (0x4e33e0, with the byte 0x5533e8 1: her location with y her head's, A+0x2480, or the location's 15 above with no active part) facing her way ((0, 0, 1) turned about y by C+0x170 less C+0x174, 0x4e3640); otherwise the camera's place (0x636518) and view (0x636524).

OSiCalc_PositionAndVolume 0x511a00 (confirmed):

  • out of hearing (false) when max^2 < |place - listener|^2 / 9 (0x52b134);
  • the ambient's minimum occlusion (+0x40) above 0.99 (0x52a03c): volume 1;
  • with the cache (the updates), the cached volume while the game time is under the record's time rounded down to 12, plus 12, plus its index mod 12;
  • else the walls on AKrCollision_Point 0x4412d0 from the place to the listener (all hits; quads of flags 0x8044800 skipped), in order from the place: none counted within 0.1 (0.01 squared, 0x529e3c) of it before the first; a quad without flag 0x1000000, or any after the first such, counts one. A run of 0x1000000 quads from the first (a door's panels) is taken as one object (AGQG +0x34): each "faces" the listener unless two-sided (0x200) when its plane's normal has a positive dot with the direction to the listener; it counts unless it faces or is within 2 (the first; 0x529818) or 8 (0x52a740, after one that faced) of the last; one that faces within 3 (9 squared, 0x52a5b0) of the last counted takes that count back; once one of them gave its object id (not -1), only quads of other ids count;
  • n walls, at most 3: max(minimum occlusion, 0.8 - n^2 / 12) (0x5296d8, 0x52b888); none: 1; no level: as 3. The time and the volume go to the cache.

At the start: no place, the sound system's start without one (0x4823a0); a place in hearing, started (0x4821f0) at that volume; out of hearing, an ambient that plays once (+0x24 bit 1) is dropped (-1), another waits. OSrUpdate runs OSiAmbient_Update 0x511f00 on every placed record each frame, and OSrAmbient_Update 0x511ec0 with a new place: out of hearing, a playing one is set to volume 0 (SSrAmbient_Update 0x4824a0) and stopped (0x481d90) unless it plays once; in hearing, a waiting one starts, a playing one's volume moves to the new one by at most 0.05 (0x52a02c, 0x52a31c), held to 0..1. A record that plays once goes when its sound has ended (the update fails); a speech line counts as playing while its record is there. SSrAmbient_Update 0x4824a0 sets the place, the velocity, the direction and the volume (+0x44 of both base channels, held to 0..2).

The level's sound objects (the spheres and volumes, OSrUpdate's playlist) are in objc.md "SNDG".

Flybys ​

A particle class with a flyby impulse (the name at +0x98 of its definition, 16 bytes; 0x456ff0 looks it up into the class's +0x8c after the level's sounds) takes one of 24 flyby records (0x61fa38, 0x28 bytes each) when it is made: the game's creation callback (0x508e20, set by P3rSetCallbacks 0x455e10) runs after each create event the particle lives through (P3rCreateEffect, P3rSendEvent, the emitters, the class change). The record (0x5156f0): bit 0 in use, the place the frame before (+4) and now (+0x10), the particle's +0x2c then (+0x1c), its class (+0x20), the particle (+0x24; its +3 the slot); none if all 24 are taken or its real place fails. OSrUpdate steps each record whose particle still has that +0x2c, frees the others; the deletion callback (0x508e90, after the die event) takes a last step and frees it (0x515a00).

The step (0x5157c0): the place moves on (a failed real place frees the record). With m the frame's move and a, b the places before and now less the listener's, d0 = a·m, d1 = b·m; when d0 × d1 < 0 the nearest point to the listener lies on the move, at (d1 p_before − d0 p_now) / (d1 − d0) (the middle if |d1 − d0| ≤ 1e-6). There the class's impulse plays (SSrImpulse_Play 0x480440, not when the scripts only), heard through the walls (0x511a00 with the impulse's max distance and min occlusion), not at all from three times its max distance. In the data only three classes have one: the Mercury Bow's bolt (w8_mbo_p01, mb_flyby), w12_ba2_p01a (benrgy_flyby) and w13_muro_p01 (muro_tr_flyby). Ported in src/sound.c (flyby_*).

Check ​

sound_test loads levels 0 and 1. It builds the databases and resolves every sound name level 1's scripts use. Then it runs a script with music and two dialogs, an impulse, a fade and a music stop for 900 ticks through the mixer, and prints the databases' numbers, when the dialogs ended and a hash of the mix.

GPL-3.0. Oni is a trademark of its owners; this project is not affiliated with them and distributes none of their data. Credits