{"name":"WSS File Format","title":"WSS File Format","type":"topic","url":"https://community.bistudio.com/wiki/WSS_File_Format","revid":378052,"updated":"2026-02-02T10:15:21Z","aliases":[],"markdown":"Bis-proprietary WSS files store Pulse Code Modulated (PCM) sound data. It's format is a variant of [RIFF WAVE file format](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html) used for .wav\n\nThere are three sound formats accepted by the engine:\n\n- **ogg**: Vorbis Stereo Music files\n- **wav**: Raw uncompressed PCM (mono or stereo)\n- **wss**: compressed PCM mono\n\nAll formats can store mono/stereo, compressed/uncompressed. The above is the **intent** of each one.\n\nDue to the Microsoft behemoth, wav files are 'industry standard' raw recording files.\nThey are as good as any other raw **recording** choice with a range of convenient-to-use tools to do so.\nWav files are also used as a standardized interchange format when converting a sound type from X to Y.\nAlthough they can be played as is, they are not intended for that purpose. They are unnecessarily huge (for the purposes of playback).\n\nBohemia Interactive introduced compressed PCM data. To that end, wss files come in three flavours:\n\n- **Uncompressed**: There is no performance, size or benefit between this, and a wav file. It is a not-normally-used, alternative to wav.\n- **Compressed Byte**: Offers a fixed, 50% reduction in file size.\n- **Compressed Nibble**: (Arrowhead only) Offers a fixed. 75% reduction in file size.\n\nThe **intent** of wss is to store compressed **mono-only** pcm data.\nThe engine and the format **allow** the other variants but that is not a preferred use (you will get rpt warnings).\n\nAs a breakdown, here are the statistics of wss types in Operation Flashpoint and Arma 2: Operation Arrowhead:\n\n| Offset | OFP | Arma 2:OA |\n|---|---|---|\n| Stereo | 1 | 19 |\n| Mono | 43 | 659 |\n| ByteCompressed | 4074 | 24197 |\n| NibbleCompressed | N/A | 156 |\n\nThe above should show that the intent has always been to compress mono only pcm data, and the newer nibble format is taking over as of pmc/baf.\nThe *reason* for mono wss files is straightforward. Almost all sounds in the game are environment, gunshot and engines.\nYou don't need a stereo wind, nor a stereo vehicle noise when the tank is running over you.\nThere is only one direction or all directions the noise is coming from.\n\n#### Faulty files\n\nA small number of BI files have errors that cause rpt errors. All are heuristically decoded by the engine anyway.\nThey are either straightforward and unintended stereo wss, or encoded nibble files that were actually not formatted properly. There are no 'bad' Operation Flashpoint files.\nGiven the stupendous number of sounds in Arma 2:OA, and given that a large quantity of wss is derived from Armed Assault, the few faulty emanations are not unreasonable.\n\n#### Tools\n\n- BI's WavToWSS.exe for encoding wav to wss\n- 3rd party tools such as *Mikero's Wss2Wav* (dead link) for translating both ways\n\n### File Format\n\n| Offset | Datatype | Content | Description |\n|---|---|---|---|\n| 0 | char[4] | \"WSS0\" | file signature |\n| 4 | ulong | CompressionType | 0 = none. 4 = Nibble compression. 8 = Byte compression |\n| 8 | ushort | format | Always 1 (WAVE_FORMAT_PCM) |\n| 10 | ushort | nChannels | 1 = mono, 2 = stereo |\n| 12 | ulong | SampleRate | e.g. 44100Hz |\n| 16 | ulong | BytesPerSecond | SampleRate * BlockAlign |\n| 20 | ushort | BlockAlign | nChannels * BytesPerSample |\n| 22 | ushort | BitsPerSecond | usually 16 |\n| 24 | ushort | Output Size | See below |\n| 26 | byte[fileSize-26] | <soundData> | Sound's PCM data |\n\n#### Input Size\n\nThe Input Size is the **bytes** remaining in the file. For uncompressed data it is (logically) always an even number (since it is a count of nShorts / 2).\n**However** many Bohemia Interactive's wss Delta4 encoded files are corrupt.\nWhile they specify uncompressed in the header, they are in fact encoded, and the only way of detecting same is by them having an odd number of bytes to decode.\nThese files cause an rpt error but the engine heuristically decodes them anyway.\n\n#### Output Size\n\nThis 'value' *was* similar in architecture at least to the wav \"datasize\" chunk. It appears to be unreliable in intent. Also, the intent appears to have changed.\n\n**Originally** it *was* very similar to the OutputSize specified by a (compressed) pbo. Specifically:\n\n- = 0 'uncompressed'. Remaining data is the size.\n- = input size. uncompressed.\n- non zero. The resultant size to extract (in bytes)\n\nThe size in fact need not be known because output size is always a defined multiple of the input size.\n\nFor Delta4 nibble compression, the value has changed to mean the Delta seed value. But this too is has been corrupted.\n\nIn general:\n\n- Uncompressed data will have this set to zero.\n- Compressed bytes will have this set to a multiple of the input size or zero.\n- Compressed Nibbles are delta seed.\n\n**None of above is reliable**\n\nNote that early versions of wss formatted files did not appear to have this field at all (for uncompressed). There is no way of telling\n\n### Decompression\n\nBohemia Interactive **optionally** uses Delta8 (byte) or Delta4 (nibble) 'Lossy' Compression for mono PCM data only.\n\nThe facility exists to handle stereo compression but it isn't employed.\n\n#### Byte Compression\n\nCompression consists of single encoded byte 'samples' versus uncompressed short 'samples'.\nEach byte is, effectively, extrapolated to a short, thus making the compressed BYTE array, and the resulting decompressed SHORT array the same number of elements(length). The length is the remaining file length (in bytes) after the header.\n\n##### C++ code\n\nFunction returns a short array, same size as it is compressed byte equivalent\n\n```cpp\n#define LOG10    2.3025850929940456840    // ln(10)\n#define LOG2    1.4426950408889634070    // log2(e)\n#define MAGIC_NUMBER    ((LOG10*LOG2)/28.12574042515172)\n\nshort* DeCompress(const char *CompressedData,int len)\n{\n    short *snap,*OutputData;\n\n    if (!(snap=OutputData=new short[len]))    return 0;\n    short LastVal = 0;\n    for (; len--; CompressedData++)\n    {\n        if (*CompressedData)\n        {\n            double asFloat = abs(*CompressedData) * MAGIC_NUMBER;\n            double rnd = Round(asFloat);\n            asFloat = pow(2.0, asFloat - rnd) * pow(2, rnd);// mantissa -\n            if (*CompressedData < 0) asFloat *= -1;\n            int asInt = Round(asFloat)+LastVal;\n            if (asInt > SHRT_MAX) asInt = SHRT_MAX ;\n            if (asInt < SHRT_MIN) asInt = SHRT_MIN;\n            LastVal=(short)asInt;\n        }\n        *OutputData++ = LastVal;\n    }\n    return snap;\n}\n```\n\n##### C# code\n\nIf the <soundData> is compressed the following (C#) code can be used for decompression:\n\n```csharp\nPCMData = new Int16[soundData.Length];\nfor (int j = 0; j < PCMData.Length; j++)\n{\n    SByte srcSample = (SByte)soundData[j];\n    if (srcSample != 0)\n    {\n        double asFloat = Math.Abs((int)srcSample) / 28.12574042515172;\n        asFloat *= 2.3025850929940456840; // ln(10)\n        asFloat *= 1.4426950408889634070; // log2(e)\n        double rnd = Math.Round(asFloat);\n        double mantisse = Math.Pow(2.0, asFloat - rnd);\n        asFloat = mantisse * Math.Pow(2, rnd);\n        if (srcSample < 0) asFloat *= -1;\n        Int32 asInt = (int)Math.Round(asFloat);\n        asInt = (j == 0) ? asInt : (asInt + PCMData[j - 1]);\n        if (asInt > short.MaxValue) asInt = short.MaxValue;\n        if (asInt < short.MinValue) asInt = short.MinValue;\n        PCMData[j] = (Int16)asInt;\n    }\n    else\n    {\n        PCMData[j] = (j == 0) ? (Int16)0 : PCMData[j - 1];\n    }\n}\n```\n\n#### Nibble Compression\n\nAlthough by no means the same thing, Nibble compression is an adaptation of IMA ADPCM compression.\nEach nibble represents a SHORT of PCMdata; thus the resultant output size will be four times larger (in bytes).\n\n##### C++ Code\n\n```cpp\n//\n// all honor and glory to T_D\n//\n\nstatic short PCMIndex[] = { -8192, -4096, -2048, -1024, -512, -256, -64, 0, 64, 256, 512, 1024, 2048, 4096, 8192 };\n\nstatic short limits(int PCM)\n{\n    if (PCM > SHRT_MAX) PCM = SHRT_MAX;\n    else if (PCM < SHRT_MIN) PCM = SHRT_MIN;\n    return PCM;\n}\n\nshort * DeCompressNibble(const char *NibbleData,int len) // len in bytes\n{\n    short *snap,*pcmData;\n    if (!(snap=pcmData=new short[2*len])) throw GENERR_NOMEM;\n    int delta = 0;\n    while (len--)\n    {\n        byte nibble = *NibbleData++;\n        *pcmData++ = limits(delta += PCMIndex[nibble >> 4]);\n        *pcmData++ = limits(delta += PCMIndex[nibble & 0x0F]);\n    }\n    return snap;\n}\n```","sections":[{"heading":null,"level":1,"markdown":"Bis-proprietary WSS files store Pulse Code Modulated (PCM) sound data. It's format is a variant of [RIFF WAVE file format](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html) used for .wav\n\nThere are three sound formats accepted by the engine:\n\n- **ogg**: Vorbis Stereo Music files\n- **wav**: Raw uncompressed PCM (mono or stereo)\n- **wss**: compressed PCM mono\n\nAll formats can store mono/stereo, compressed/uncompressed. The above is the **intent** of each one.\n\nDue to the Microsoft behemoth, wav files are 'industry standard' raw recording files.\nThey are as good as any other raw **recording** choice with a range of convenient-to-use tools to do so.\nWav files are also used as a standardized interchange format when converting a sound type from X to Y.\nAlthough they can be played as is, they are not intended for that purpose. They are unnecessarily huge (for the purposes of playback).\n\nBohemia Interactive introduced compressed PCM data. To that end, wss files come in three flavours:\n\n- **Uncompressed**: There is no performance, size or benefit between this, and a wav file. It is a not-normally-used, alternative to wav.\n- **Compressed Byte**: Offers a fixed, 50% reduction in file size.\n- **Compressed Nibble**: (Arrowhead only) Offers a fixed. 75% reduction in file size.\n\nThe **intent** of wss is to store compressed **mono-only** pcm data.\nThe engine and the format **allow** the other variants but that is not a preferred use (you will get rpt warnings).\n\nAs a breakdown, here are the statistics of wss types in Operation Flashpoint and Arma 2: Operation Arrowhead:\n\n| Offset | OFP | Arma 2:OA |\n|---|---|---|\n| Stereo | 1 | 19 |\n| Mono | 43 | 659 |\n| ByteCompressed | 4074 | 24197 |\n| NibbleCompressed | N/A | 156 |\n\nThe above should show that the intent has always been to compress mono only pcm data, and the newer nibble format is taking over as of pmc/baf.\nThe *reason* for mono wss files is straightforward. Almost all sounds in the game are environment, gunshot and engines.\nYou don't need a stereo wind, nor a stereo vehicle noise when the tank is running over you.\nThere is only one direction or all directions the noise is coming from."},{"heading":"Faulty files","level":4,"markdown":"A small number of BI files have errors that cause rpt errors. All are heuristically decoded by the engine anyway.\nThey are either straightforward and unintended stereo wss, or encoded nibble files that were actually not formatted properly. There are no 'bad' Operation Flashpoint files.\nGiven the stupendous number of sounds in Arma 2:OA, and given that a large quantity of wss is derived from Armed Assault, the few faulty emanations are not unreasonable."},{"heading":"Tools","level":4,"markdown":"- BI's WavToWSS.exe for encoding wav to wss\n- 3rd party tools such as *Mikero's Wss2Wav* (dead link) for translating both ways"},{"heading":"File Format","level":3,"markdown":"| Offset | Datatype | Content | Description |\n|---|---|---|---|\n| 0 | char[4] | \"WSS0\" | file signature |\n| 4 | ulong | CompressionType | 0 = none. 4 = Nibble compression. 8 = Byte compression |\n| 8 | ushort | format | Always 1 (WAVE_FORMAT_PCM) |\n| 10 | ushort | nChannels | 1 = mono, 2 = stereo |\n| 12 | ulong | SampleRate | e.g. 44100Hz |\n| 16 | ulong | BytesPerSecond | SampleRate * BlockAlign |\n| 20 | ushort | BlockAlign | nChannels * BytesPerSample |\n| 22 | ushort | BitsPerSecond | usually 16 |\n| 24 | ushort | Output Size | See below |\n| 26 | byte[fileSize-26] | <soundData> | Sound's PCM data |"},{"heading":"Input Size","level":4,"markdown":"The Input Size is the **bytes** remaining in the file. For uncompressed data it is (logically) always an even number (since it is a count of nShorts / 2).\n**However** many Bohemia Interactive's wss Delta4 encoded files are corrupt.\nWhile they specify uncompressed in the header, they are in fact encoded, and the only way of detecting same is by them having an odd number of bytes to decode.\nThese files cause an rpt error but the engine heuristically decodes them anyway."},{"heading":"Output Size","level":4,"markdown":"This 'value' *was* similar in architecture at least to the wav \"datasize\" chunk. It appears to be unreliable in intent. Also, the intent appears to have changed.\n\n**Originally** it *was* very similar to the OutputSize specified by a (compressed) pbo. Specifically:\n\n- = 0 'uncompressed'. Remaining data is the size.\n- = input size. uncompressed.\n- non zero. The resultant size to extract (in bytes)\n\nThe size in fact need not be known because output size is always a defined multiple of the input size.\n\nFor Delta4 nibble compression, the value has changed to mean the Delta seed value. But this too is has been corrupted.\n\nIn general:\n\n- Uncompressed data will have this set to zero.\n- Compressed bytes will have this set to a multiple of the input size or zero.\n- Compressed Nibbles are delta seed.\n\n**None of above is reliable**\n\nNote that early versions of wss formatted files did not appear to have this field at all (for uncompressed). There is no way of telling"},{"heading":"Decompression","level":3,"markdown":"Bohemia Interactive **optionally** uses Delta8 (byte) or Delta4 (nibble) 'Lossy' Compression for mono PCM data only.\n\nThe facility exists to handle stereo compression but it isn't employed."},{"heading":"Byte Compression","level":4,"markdown":"Compression consists of single encoded byte 'samples' versus uncompressed short 'samples'.\nEach byte is, effectively, extrapolated to a short, thus making the compressed BYTE array, and the resulting decompressed SHORT array the same number of elements(length). The length is the remaining file length (in bytes) after the header."},{"heading":"C++ code","level":5,"markdown":"Function returns a short array, same size as it is compressed byte equivalent\n\n```cpp\n#define LOG10    2.3025850929940456840    // ln(10)\n#define LOG2    1.4426950408889634070    // log2(e)\n#define MAGIC_NUMBER    ((LOG10*LOG2)/28.12574042515172)\n\nshort* DeCompress(const char *CompressedData,int len)\n{\n    short *snap,*OutputData;\n\n    if (!(snap=OutputData=new short[len]))    return 0;\n    short LastVal = 0;\n    for (; len--; CompressedData++)\n    {\n        if (*CompressedData)\n        {\n            double asFloat = abs(*CompressedData) * MAGIC_NUMBER;\n            double rnd = Round(asFloat);\n            asFloat = pow(2.0, asFloat - rnd) * pow(2, rnd);// mantissa -\n            if (*CompressedData < 0) asFloat *= -1;\n            int asInt = Round(asFloat)+LastVal;\n            if (asInt > SHRT_MAX) asInt = SHRT_MAX ;\n            if (asInt < SHRT_MIN) asInt = SHRT_MIN;\n            LastVal=(short)asInt;\n        }\n        *OutputData++ = LastVal;\n    }\n    return snap;\n}\n```"},{"heading":"C# code","level":5,"markdown":"If the <soundData> is compressed the following (C#) code can be used for decompression:\n\n```csharp\nPCMData = new Int16[soundData.Length];\nfor (int j = 0; j < PCMData.Length; j++)\n{\n    SByte srcSample = (SByte)soundData[j];\n    if (srcSample != 0)\n    {\n        double asFloat = Math.Abs((int)srcSample) / 28.12574042515172;\n        asFloat *= 2.3025850929940456840; // ln(10)\n        asFloat *= 1.4426950408889634070; // log2(e)\n        double rnd = Math.Round(asFloat);\n        double mantisse = Math.Pow(2.0, asFloat - rnd);\n        asFloat = mantisse * Math.Pow(2, rnd);\n        if (srcSample < 0) asFloat *= -1;\n        Int32 asInt = (int)Math.Round(asFloat);\n        asInt = (j == 0) ? asInt : (asInt + PCMData[j - 1]);\n        if (asInt > short.MaxValue) asInt = short.MaxValue;\n        if (asInt < short.MinValue) asInt = short.MinValue;\n        PCMData[j] = (Int16)asInt;\n    }\n    else\n    {\n        PCMData[j] = (j == 0) ? (Int16)0 : PCMData[j - 1];\n    }\n}\n```"},{"heading":"Nibble Compression","level":4,"markdown":"Although by no means the same thing, Nibble compression is an adaptation of IMA ADPCM compression.\nEach nibble represents a SHORT of PCMdata; thus the resultant output size will be four times larger (in bytes)."},{"heading":"C++ Code","level":5,"markdown":"```cpp\n//\n// all honor and glory to T_D\n//\n\nstatic short PCMIndex[] = { -8192, -4096, -2048, -1024, -512, -256, -64, 0, 64, 256, 512, 1024, 2048, 4096, 8192 };\n\nstatic short limits(int PCM)\n{\n    if (PCM > SHRT_MAX) PCM = SHRT_MAX;\n    else if (PCM < SHRT_MIN) PCM = SHRT_MIN;\n    return PCM;\n}\n\nshort * DeCompressNibble(const char *NibbleData,int len) // len in bytes\n{\n    short *snap,*pcmData;\n    if (!(snap=pcmData=new short[2*len])) throw GENERR_NOMEM;\n    int delta = 0;\n    while (len--)\n    {\n        byte nibble = *NibbleData++;\n        *pcmData++ = limits(delta += PCMIndex[nibble >> 4]);\n        *pcmData++ = limits(delta += PCMIndex[nibble & 0x0F]);\n    }\n    return snap;\n}\n```"}],"categories":["Real Virtuality File Formats"],"groups":["Tools and File Formats"]}