Codec for the game's gun audio pack format and an MS ADPCM-to-PCM16 WAV decoder. It reads a .gunpack header, locates indexed WAV blobs, validates WAV fmt/data chunks, supports PCM16 passthrough and decodes Microsoft ADPCM (4-bit) into interleaved 16-bit little-endian PCM for mono or stereo.
using System;
namespace SWB.Base;
/// <summary>
/// The gun audio pack file format, and the one recording format inside it — package trim step 4 (2026-10-03,
/// Docs/STEP4_GUN_PACKS.md). Plain C#, nothing from the engine, so the same file is compiled into the scratch test that
/// checks it against ffmpeg.
///
/// A pack (`sounds/weapons/_packs/*.gunpack`, written by `Tools/gun_packs.py`):
/// "NZGP", version (u32), index length N (u32), N bytes of UTF-8 JSON `{"entries": [[name, offset, length], …]}`,
/// then the recordings' WAV files exactly as they were, back to back. An offset counts from the end of the index.
///
/// ⛔ MS ADPCM IS DECODED HERE, NOT BY THE ENGINE. 7,787 of the 7,792 gun WAVs are MS ADPCM (4-bit). The engine's WAV
/// reader (`SoundFile.FromWav`) is PCM-only and its compressed-data entry point (`SoundFile.Create`) is internal, so a
/// recording becomes 16-bit PCM here and reaches the engine through `SoundFile.FromPcm`. Checked sample for sample
/// against ffmpeg on 300 recordings spread over every gun, before any gun used it.
/// ⚠️ PCM 16 PASSES THROUGH. Anything else (another codec, 3+ channels) returns null; the converter never packs those.
/// </summary>
public static class GunPackCodec
{
public const int Version = 1;
static readonly int[] Adapt = { 230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230 };
static readonly int[] StdCoef1 = { 256, 512, 0, 192, 240, 460, 392 };
static readonly int[] StdCoef2 = { 0, -256, 0, 64, 0, -208, -232 };
static int U16( byte[] b, int i ) => b[i] | (b[i + 1] << 8);
static int S16( byte[] b, int i ) => (short)(b[i] | (b[i + 1] << 8));
public static int I32( byte[] b, int i ) => b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24);
/// <summary>A pack's header: is it one, and where its index and data are.</summary>
public static bool ReadHeader( byte[] pack, out int version, out int indexStart, out int indexLength )
{
version = indexStart = indexLength = 0;
if ( pack is null || pack.Length < 12 || pack[0] != 'N' || pack[1] != 'Z' || pack[2] != 'G' || pack[3] != 'P' ) return false;
version = I32( pack, 4 );
indexLength = I32( pack, 8 );
indexStart = 12;
return indexLength >= 0 && indexStart + indexLength <= pack.Length;
}
/// <summary>
/// One recording (its WAV bytes, at <paramref name="start"/> in <paramref name="b"/>) as interleaved 16-bit PCM,
/// little-endian. Null when it is not MS ADPCM or PCM 16, mono or stereo.
/// </summary>
public static byte[] Decode( byte[] b, int start, int length, out int channels, out int rate )
{
channels = 0; rate = 0;
int end = start + length;
if ( b is null || length < 12 || end > b.Length || b[start] != 'R' || b[start + 1] != 'I' || b[start + 2] != 'F' || b[start + 3] != 'F' )
return null;
int pos = start + 12, tag = 0, align = 0, bits = 0, ncoef = 0, fmt = -1, data = -1, dataLen = 0;
while ( pos + 8 <= end )
{
int size = I32( b, pos + 4 );
if ( size < 0 ) break;
if ( b[pos] == 'f' && b[pos + 1] == 'm' && b[pos + 2] == 't' && b[pos + 3] == ' ' )
{
fmt = pos + 8;
tag = U16( b, fmt ); channels = U16( b, fmt + 2 ); rate = I32( b, fmt + 4 );
align = U16( b, fmt + 12 ); bits = U16( b, fmt + 14 );
if ( tag == 2 && size >= 22 ) ncoef = U16( b, fmt + 20 );
}
else if ( b[pos] == 'd' && b[pos + 1] == 'a' && b[pos + 2] == 't' && b[pos + 3] == 'a' )
{
data = pos + 8;
dataLen = Math.Min( size, end - data );
}
pos += 8 + size + (size & 1);
}
if ( fmt < 0 || data < 0 || channels < 1 || channels > 2 || rate <= 0 ) return null;
if ( tag == 1 && bits == 16 )
{
var copy = new byte[dataLen & ~1];
Array.Copy( b, data, copy, 0, copy.Length );
return copy;
}
if ( tag != 2 || align <= 0 ) return null;
// the file's own coefficient table; every gun WAV carries the standard seven
int n = Math.Max( ncoef, 7 );
var c1 = new int[n];
var c2 = new int[n];
for ( int i = 0; i < n; i++ )
{
bool own = i < ncoef && fmt + 26 + i * 4 <= end;
c1[i] = own ? S16( b, fmt + 22 + i * 4 ) : StdCoef1[i];
c2[i] = own ? S16( b, fmt + 24 + i * 4 ) : StdCoef2[i];
}
int ch = channels, hdr = 7 * ch;
long frames = 0;
for ( int o = 0; o < dataLen; o += align )
{
int len = Math.Min( align, dataLen - o );
if ( len >= hdr ) frames += 2 + (len - hdr) * 2 / ch;
}
var pcm = new byte[frames * ch * 2];
int w = 0;
for ( int o = 0; o < dataLen; o += align )
{
int len = Math.Min( align, dataLen - o );
if ( len < hdr ) break;
int k = data + o;
if ( ch == 1 )
{
int p = Math.Min( b[k], n - 1 ), a1 = c1[p], a2 = c2[p];
int d = S16( b, k + 1 ), s1 = S16( b, k + 3 ), s2 = S16( b, k + 5 );
Put( pcm, ref w, s2 );
Put( pcm, ref w, s1 );
for ( int i = k + 7; i < k + len; i++ )
{
int by = b[i];
Put( pcm, ref w, Nibble( ref s1, ref s2, ref d, a1, a2, by >> 4 ) );
Put( pcm, ref w, Nibble( ref s1, ref s2, ref d, a1, a2, by & 15 ) );
}
}
else
{
int pl = Math.Min( b[k], n - 1 ), pr = Math.Min( b[k + 1], n - 1 );
int l1 = c1[pl], l2 = c2[pl], r1 = c1[pr], r2 = c2[pr];
int dl = S16( b, k + 2 ), dr = S16( b, k + 4 );
int s1l = S16( b, k + 6 ), s1r = S16( b, k + 8 ), s2l = S16( b, k + 10 ), s2r = S16( b, k + 12 );
Put( pcm, ref w, s2l ); Put( pcm, ref w, s2r );
Put( pcm, ref w, s1l ); Put( pcm, ref w, s1r );
for ( int i = k + 14; i < k + len; i++ )
{
int by = b[i];
Put( pcm, ref w, Nibble( ref s1l, ref s2l, ref dl, l1, l2, by >> 4 ) );
Put( pcm, ref w, Nibble( ref s1r, ref s2r, ref dr, r1, r2, by & 15 ) );
}
}
}
return pcm;
}
static void Put( byte[] pcm, ref int w, int s )
{
pcm[w++] = (byte)s;
pcm[w++] = (byte)(s >> 8);
}
/// <summary>One 4-bit step, as the MS ADPCM reference and ffmpeg do it: `/ 256` truncates toward zero.</summary>
static int Nibble( ref int s1, ref int s2, ref int delta, int c1, int c2, int nib )
{
int pred = (s1 * c1 + s2 * c2) / 256 + ((nib & 8) != 0 ? nib - 16 : nib) * delta;
if ( pred > 32767 ) pred = 32767; else if ( pred < -32768 ) pred = -32768;
s2 = s1;
s1 = pred;
delta = (Adapt[nib] * delta) >> 8;
if ( delta < 16 ) delta = 16;
return pred;
}
}