OBD:SNDD
|
There are 2 different formats used by the SNDD files. Both versions use a form of ADPCM compression (Adaptive Differential Pulse-Code Modulation), where 16-bit sound samples are encoded as 4-bit nibbles (resulting roughly in a 4:1 compression ratio as compared to uncompressed 16-bit PCM).
- In PC retail Oni versions, sounds are encoded using Microsoft's ADPCM algorithm described HERE. FFmpeg lists this codec as adpcm_ms.
- For PC demo and Mac, sounds are encoded using the IMA4 algorithm described HERE. FFmpeg lists this codec as adpcm_ima_qt.
The Mac/demo SNDDs come short of PC retail SNDDs in the following respects:
- Mac/demo SNDDs only support one sample rate (22.05 kHz). PC retail supports arbitrary sample rate, allowing 46 electric spark sounds to use CD-quality 44.1 kHz (for crisper high frequencies).
- At 22.05 kHz, Mac/demo SNDDs are about 5% larger than PC retail equivalents, because of a much smaller block size in the .raw part (the .dat part is smaller on Mac/demo, but this doesn't help).
- Mac/demo SNDDs have encoding/editing artifacts at the ends of looping segments (music and ambient tracks). Only PC retail SNDDs can be pieced together seamlessly. See BELOW for details.
Oni storage
PC retail
Below is the .dat file part used in the PC retail version.
Offset | Type | Raw Hex | Value | Description |
---|---|---|---|---|
0x00 | res_id | 01 D7 08 00 | 2263 | 02263-comguy_dth2.aif.SNDD |
0x04 | lev_id | 01 00 00 06 | 3 | level 3 |
0x08 | int32 | 08 00 00 00 | 8 | flags
|
0x0C | block[50] | wav header (corresponds to the "fmt " section of a RIFF WAVE file) | ||
0x3E | int16 | 37 00 | 55 | duration in 1/60 seconds (game ticks) |
0x40 | int32 | 56 28 00 00 | 10326 | size of the part in the raw file in bytes |
0x44 | offset | 20 10 59 00 | 0x591020 | at this position starts the part in the raw file |
0x48 | char[24] | AD DE | dead | 24 unused bytes (padding) |
The .raw data contains the actual audio sample blocks without any other headers (other than block headers).
- MS ADPCM format details
- For a detailed overview of the ADPCM algorithm (if interested), see HERE. For an actual implementation example, see, e.g., FFmpeg.
- The MS ADPCM data has 512- or 1024-byte blocks (512 bytes for 22.05 kHz mono, 1024 bytes for 22.05 kHz stereo and 44.1 kHz mono)
- Each block consists of a 7- or 14-byte header (7 bytes for mono, 14 bytes for stereo), which includes the block's first two samples.
- The remaining 505, 1010 or 1017 bytes of each block consist of nibbles (half-bytes), with left-right interleaving in the case of stereo.
- (that's 1010 more samples in the case of 22.05 kHz mono or stereo, and 2034 more samples in the case of 44.1 kHz mono)
- Thus the total number of samples per block (including the two in the header) is 1012 for 22.05 kHz (mono or stereo) and 2036 for 44.1 kHz mono.
- The final block in the file can be incomplete (the decoder can infer this from the block size and raw data size).
PC demo and Mac
The Mac version and the PC demo version use a simpler format, with no support for different sample rates (all sounds are sampled at 22050 Hz).
Offset | Type | Raw Hex | Value | Description |
---|---|---|---|---|
0x00 | res_id | 01 D6 08 00 | 2262 | 02262-comguy_dth2.aif.SNDD |
0x04 | lev_id | 01 00 00 06 | 3 | level 3 |
0x08 | int32 | 01 00 00 00 | 1 | flags
|
0x0C | int32 | 37 00 00 00 | 55 | duration in 1/60 seconds (game ticks) |
0x10 | int32 | 5E 2A 00 00 | 10846 | size of the part in the raw file in bytes |
0x14 | offset | 00 B1 01 00 | 0x1B100 | at this position starts the part in the raw file |
0x18 | char[8] | AD DE | dead | 8 unused bytes (padding) |
The .raw data contains the actual audio sample blocks without any other headers (other than block headers).
- IMA ADPCM (IMA4) - Mac and PC demo
- For an overview of the IMA ADPCM algorithm and IMA4 header (if interested), see HERE
- The IMA4 ADPCM data has 34-byte blocks (in the case of stereo, there is an even number of such blocks, because Left and Right blocks are interleaved).
- The first two bytes of each block are used to set the initial predictor (upper 9 bits) and step (lower 7 bits) for decoding the block's samples.
- The other 32 bytes consist of 64 samples stored as nibbles (half-bytes). In the case of stereo, all the nibbles in a block belong to the same channel (either all Left or all Right).
- Unlike for MS ADPCM, incomplete trailing blocks (if any) are not announced in any way: the final blocks are stored in their entirety, with no way to tell how much of it is actual data.
- For this reason, identical sounds do not have the same sample count on PC retail and Mac/demo. As an example, here are the stats for some stereo sounds ("atm_cl05" ambient):
|
By looking at the end of the Mac/demo SNDDs (or the exported AIFF files), it can be confirmed that the extraneous samples are actually there, at the end of the last two 34-byte blocks (last Left block and last Right block). Unless the Mac and/or PC demo engines have a very non-standard implementation of IMA4 ADPCM, there is no way to interrupt playback for the lask blocks by cutting off the trailing samples (because they are no different from regular smaples). This is one of the aspects that impact seamless playback of sequences (music or ambient tracks), see "looping issues" below.
Possibly Mac/demo Oni just looks at the approximate length of each SNDD in frames (or game ticks, i.e., 1/60th of a second), which is listed in each SNDD's header and, once the announced frame count has been reached for the currently playing sound, starts playback on the next sound in the sequence. Depending on the hardware/software implementation of the audio pipelines, this logic can either interrupt the currently playing sound, or cause a slight overlap/crossfade between the current sound and the next. It is possible that PC retail Oni actually does the same, i.e., segments of a sequence are dispatched to the OS based on the frame count of the previous segment, rather than based on its actual play time (sample count). The early interrupt/crossfade is barely noticeable.
Another theoretical possibility is that, in the case of IMA4 ADPCM, an illegal step index (outside the expected 0-88 range) is used to signify the end of the stream. Decoders typically resolve this by forcing out-of-bounds step indices into the 0-88 interval, but perhaps a custom decoder can interrupt the stream instead. However, this would be very non-standard behavior, and would only be feasible if the decompressed audio stream is put together inside Oni's engine, rather than deferred to the OS. Therefore it is more likely that Oni dispatches each segment to the OS based on the frame count; the OS receives the ADPCM-compressed data, decompresses it and plays it back; as for the overlap/crossfade/interruption of the currently playing segment, it is handled at OS level.
Exporting and importing tips
To create a wav/aif file one needs to write a file header like below and then write the contents of the raw data part.
WAV files (from PC retail SNDDs)
- Write "RIFF"
- add the size of the part in the raw file + 70 bytes
- write "WAVE"
- write "fmt "
- write 50
- write the wav header
- OPTIONAL/RECOMMENDED: compute the number of samples and add a "fact" section announcing it
- write "data"
- add the size of the part in the raw file OPTIONAL/RECOMMENDED: increase the size if the last sample block is incomplete
- add the raw file data OPTIONAL/RECOMMENDED: add padding to the last sample block if it is incomplete
- save it as a wav file.
Offset | Type | Raw Hex | Value | Description |
---|---|---|---|---|
Complete ADPCM wav format header (black outline) | ||||
0x00 | char[4] | 52 49 46 46 | RIFF | identifier for the "IBM/Microsoft RIFF" standard |
0x04 | int32 | 9C 28 00 00 | 10396 | size of the file from 0x08 to the end (= size of the .raw part + 70 bytes) |
0x08 | char[4] | 57 41 56 45 | WAVE | identifier for the "WAVE" format |
0x0C | char[4] | 66 6D 74 20 | "fmt " | identifier announcing the following wav format header |
0x10 | int32 | 32 00 00 00 | 50 | wave format header size |
0x14 | block[50] | wav header | ||
0x46 | char[4] | 64 61 74 61 | data | identifier announcing the following wav data |
0x4A | int32 | 56 28 00 00 | 10326 | size of the following wav data in bytes (= size of the .raw part) |
The above is not 100% consistent with the WAVE storage rules, because it allows for a completely arbitrary "data" size. Microsoft ADPCM data is supposed to be stored as a number of fixed-size blocks (in Oni, each block is either 512 bytes for 22.05 kHz mono, or 1024 bytes for 22.05 kHz stereo and 44.1 kHz mono). Thus, according to the standard, the last block - even if incomplete - must be stored in its entirety, and the "data" size must be a multiple of the block size. In the above example, since the format is 22.05 kHz mono, the "data" size should be increased from 10326 to 10752=21x512, and 426 empty bytes should be added as padding, so that there are 21 complete data blocks.
The standard way to deal with incomplete blocks is to specify not just the data size, but the actual number of samples, by adding a "fact" section to the WAVE header, like this:
Offset | Type | Raw Hex | Value | Description |
---|---|---|---|---|
Complete ADPCM wav format header | ||||
0x00 | char[4] | 52 49 46 46 | RIFF | identifier for the "IBM/Microsoft RIFF" standard |
0x04 | int32 | 9C 28 00 00 | 10396 | size of the file from 0x08 to the end (= size of the .raw part + 70 bytes) |
0x08 | char[4] | 57 41 56 45 | WAVE | identifier for the "WAVE" format |
0x0C | char[4] | 66 6D 74 20 | "fmt " | identifier announcing the following wav format header section |
0x10 | int32 | 32 00 00 00 | 50 | wave format header size |
0x14 | block[50] | wav header | ||
0x46 | char[4] | 66 61 63 74 | fact | identifier announcing the following "fact" section |
0x4A | int32 | 04 00 00 00 | 4 | size of the following "fact" section in bytes |
0x4E | int32 | B0 4F 00 00 | 20400 | actual number of samples (see below for calculation) |
0x52 | char[4] | 64 61 74 61 | data | identifier announcing the following wav data |
0x56 | int32 | 00 2A 00 00 | 10752 | size of the following wav data in bytes (= size of the .raw part + 426 empty bytes) |
The actual number of samples is implied from the actual data size (size of the .raw part) and wav header properties as follows:
- n_whole_blocks = floor(raw_size/block_size); // EXAMPLE: floor(10326/512) = 20
- last_block_size = raw_size - whole_blocks*block_size; // EXAMPLE: 10326 - 20x512 = 86
- last_block_samples = (last_block_size - 7*n_channels)*(8/bits_per_sample/n_channels) + 2; // EXAMPLE: (86 - 7)*(8/4) + 2 = 160
- n_samples = n_whole_blocks*samples_per_block + last_block_samples; // EXAMPLE: 20*1012 + 160 = 20400
AIF files (from Mac/demo SNDDs)
- Write "FORM"
- add the size of the part in the raw file + 50 bytes
- write "AIFC"
- write "COMM "
- add the aif header (after filling in in, the number of channels, the sample rate - always 22.05 kHz -, the bits per sample - always 16 - and the number of sample frames/blocks)
- write "SSND"
- add the size of the part in the raw file + 8 bytes
- add 8 zero bytes (custom "offset" and "block size" fields)
- add the raw file data and save it as an aif file.
Note the Big Endian order
Offset | Type | Raw Hex | Value | Description |
---|---|---|---|---|
Complete aif format header (black outline) | ||||
0x00 | char[4] | 46 4F 52 4D | FORM | identifier for the "EA IFF 85" standard |
0x04 | int32 | 00 00 2A 90 | 10896 | size of the file from 0x08 to the end (= size of the .raw part + 50 bytes) |
0x08 | char[4] | 41 49 46 43 | AIFC | identifier for the "AIFC" format (compressed aif file) |
0x0C | char[4] | 43 4F 4D 4D | COMM | identifier announcing the following aif format header |
0x10 | block[26] | aif header | ||
0x2A | char[4] | 53 53 4E 44 | SSND | identifier announcing the following aif data |
0x2E | int32 | 00 00 2A 66 | 10854 | size of the file from 0x32 to the end (= size of the .raw part + 8 bytes) |
0x32 | int32 | 00 00 00 00 | 0 | offset; determines where the first sample in the data starts; use zero |
0x36 | int32 | 00 00 00 00 | 0 | block size; used in conjunction with offset for block-aligning data; use zero |
Looping issues
As detailed above, ADPCM data is stored in blocks, but the actual sound data does not necessarily end exactly at the end of a block. This is true both for MS ADPCM (PC retail) and IMA4 ADPCM (Mac and PC demo), but is especially noticeable for the comparatively large blocks of MS ADPCM, where the padding can be as large as ~1010 samples, i.e., a ~46-millisecond silence in the case of 22.05 kHz (for IMA4, the biggest possible gap is 63 samples, or ~3 milliseconds).
MS ADPCM
Although the final block of a MS ADPCM SNDD file (PC retail) is stored in incomplete form (with only the actual samples and no padding), the standard decoding behavior (e.g., in an audio editor) is to automatically add the padding up to the end of the last block of an ADPCM-compressed WAV. Depending on the audio-editing tool, this can create a silence or some "bad data" at the end of the imported audio, which can be a problem if one wants to join SNDDs that are supposed to play seamlessly one after another (e.g., a musical or ambient sequence).
As a workaround, one can preprocess .wav files with some tools that are more flexible about incomplete MS ADPCM blocks:
- For Sox, padding is disabled by default when joining several files.
- For ffmpeg, padding can be disabled as an optional setting.
As an actual solution, the .wav file should be compliant with RIFF WAVE standards, i;e., the last block should be padded to its full size, and a "fact" section should be used to specify the actual number of samples. This is implemented in OniSplit v 0.9.###
Slight distorsions are sometimes observed near the ends of looping SNDDs (music and ambient tracks). These artifacts were likely caused by Bungie's audio tools, and can not be undone automatically. Barely noticeable, they can be healed by manually editing audio samples near the seams.
IMA ADPCM
Padding
In the case of IMA ADPCM, the padding is actually present in the stored audio, so it is impossible (both for OniSplit and for a third-party converter) to automatically trim it down to just the relevant audio data. In fact, just by looking at the Mac/demo SNDD itself, there is no way to tell how many of the trailing samples need to be cut for a truly seamless transition.
There are two solutions - an approximate one and an exact one:
- As an approximation, look up the frame count announced in the SNDD's header (using a hex viewer or an XML dump), divide that by 60 to get the length of the clip in seconds, and multiply by the sample rate 22.05 kHz. You will get the number of samples (and the corresponding delay in seconds) that are actually played back by Oni before starting the next sound in the sequence. You can replicate this delay with ffmpeg, Sox, or the audio editor of your choice. A cross-fade between the two clips in the overlapping region should sound best. You can also examine the samples near the approximate transition time, and "manually" determine where the actual samples end and the padding begins.
- As an exact solution, look up the sample count of an equivalent MS ADPCM file from a PC retail version of Oni. Padding should only be a problem for non-localized music and ambients, so the language version shouldn't matter. The electric "zap" sounds (which are sampled at 44.1 kHz in PC retail Oni) are also not loopable, so you should be able to find a 22.05 kHz sound with a intuitive sample count, similar to the Mac /demo version. It will be somewhat smaller than the raw sample count of the IMA4 ADPCM, because of the padding (see the atm_cl05 example above for a comparison). Once you know the actual sample count, use ffmpeg to convert the .aif file to .wav (either PCM or ADPCM), keeping only the actual samples and trimming out the padding. Then, use Sox or ffmpeg to seamlessly join the .wav files as you would for regular MS ADPCM files (see above).
Of course, you can just grab a PC retail copy of Oni and extract the MS ADPCM sounds directly from there.
Initial transient
The biggest problem with seamless playback Mac/demo SNDDs (for music and ambient tracks) is that - even if you figure out the correct length of each segment - the waveform of each next segment builds up from zero over ~7 samples, instead of picking up where the previous segment left. This introduces about 0.3 milliseconds of silence, and an audible discontinuity in the waveform, even if the two segments are lined up properly. The values of those initial samples is not recoverable, so again it is recommended to turn to a PC retail copy.
PCM export
OniSplit v0.9.### implements export to uncompressed PCM (signed 16-bit linear) from both the PC retail and the Mac/demo SNDD format: use -extract:pcm instead of either -extract:wav or -extract:aif. As compared to ADPCM, linear PCM is a much more straightforward format (almost human readable), and makes it easier to analyze artifacts.
Note, however, that transcoding (between IMA4 and MS ADPCM) and encoding is not implemented at this point. So -extract:aif will not work on PC retail SNDDs, -extract:wav will not work on Mac/demo SNDDs, and -create will only work on sound files that use the correct codec and sample rate supported by the PC retail or Mac/demo Oni engine.
ONI BINARY DATA |
---|
QTNA << Other file types >> StNA |
SNDD : Sound Data |
Generic file |