Draft reply for https://trac.ffmpeg.org/ticket/6091
I've fully reverse-engineered the DS2 (DSS Pro) codec from Olympus's DssDecoder.dll / AudioSDK DLL using Ghidra, and built a working open-source decoder that produces output matching the proprietary DLLs (1.0000 correlation, bit-exact on all test files). Posting the complete findings here so this can finally get proper FFmpeg support.
Reference implementation: https://github.com/hirparak/dss-codec (Rust, MIT-licensed) — all algorithms, tables, and test vectors included.
The current -f dss path fails for three independent reasons:
- Wrong sample rate: FFmpeg uses 11025 Hz. DS2 SP is 12000 Hz, DS2 QP is 16000 Hz.
- Different codec tables: None of FFmpeg's
dss_sptables (filter_cb, fixed_cb_gain, pulse_val, adaptive_gain, sinc) exist in the Olympus DLL. The codecs share a CELP architecture but every parameter differs. - Different demuxing: DS2 SP uses a byte-swap packet scheme not present in FFmpeg's DSS demuxer. DS2 QP uses a continuous bitstream across blocks with no per-frame packet boundaries.
DS2 files have magic bytes \x03ds2 (vs DSS: \x02dss or \x03dss).
The codec mode is determined by byte 4 of the first audio block (at file offset 0x604):
| byte4 value | Codec | Sample rate | Bitrate |
|---|---|---|---|
| 0-1 | DS2 SP | 12000 Hz | ~13.7 kbps |
| 2-5 | LP (unimplemented) | 8000 Hz | — |
| 6-7 | DS2 QP | 16000 Hz | ~28 kbps |
- Header: 1536 bytes (0x600)
- Audio data: sequence of 512-byte blocks starting at offset 0x600
- Block header: 6 bytes per block (
byte0 byte1 frame_count 0xFF format_type 0xFF)byte0bit 7: swap initialization flag (SP mode only)byte1: continuation size parameter (DSS/SP only, for empty block handling)byte2: number of frames starting in this blockbyte4: format type (0=SP, 6=QP)
- Block payload: 506 bytes of audio data per block
Total frame count = sum of byte2 across all blocks.
Both DS2 codecs use an MSB-first-within-16-bit-LE-words bitstream reader:
- Read 16-bit words in little-endian byte order
- Within each word, consume bits from MSB (bit 15) to LSB (bit 0)
- Fields span word boundaries seamlessly
This is the same bit order as FFmpeg's existing DSS SP reader.
| Parameter | DS2 SP | FFmpeg dss_sp |
|---|---|---|
| Sample rate | 12000 Hz | 11025 Hz |
| Subframe size | 72 samples | 66 samples |
| Subframes/frame | 4 | 4 |
| Samples/frame | 288 | 264 |
| Reflection coeffs | 14 | 12 |
| Pitch range | 36–186 | 20–143 |
| Excitation pulses | 7 | 6 |
| Codebook | C(72,7) combinatorial | different |
| Frame bits | 328 | 264 |
| Packet size | 42 bytes | 42 bytes |
| Synthesis filter | Lattice (f64) | Direct-form |
SP mode uses a byte-swap alternation scheme similar to FFmpeg's existing DSS demuxer, but the swap logic differs:
- Strip 6-byte block headers, concatenate 506-byte payloads into a flat stream.
- Initialize swap state from bit 7 of the first block's byte0.
- Alternate between reading 42-byte (no-swap) and 40-byte (swap) chunks:
- No-swap frame: Read 42 bytes directly as the packet. Save byte[40] as the swap byte.
- Swap frame: Read 40 bytes into
pkt[3..43], then shift even-indexed bytes:pkt[i] = pkt[i+4]for i in 0..40 step 2. Setpkt[1] = swap_byte.
- In both cases, zero out
pkt[40]before decoding. - Toggle swap state after each frame.
refl[0]: 5 bits (codebook 0, 32 entries)
refl[1]: 5 bits (codebook 1, 32 entries)
refl[2]: 4 bits (codebook 2, 16 entries)
refl[3]: 4 bits (codebook 3, 16 entries)
refl[4]: 4 bits (codebook 4, 16 entries)
refl[5]: 4 bits (codebook 5, 16 entries)
refl[6]: 4 bits (codebook 6, 16 entries)
refl[7]: 4 bits (codebook 7, 16 entries)
refl[8]: 3 bits (codebook 8, 8 entries)
refl[9]: 3 bits (codebook 9, 8 entries)
refl[10]: 3 bits (codebook 10, 8 entries)
refl[11]: 3 bits (codebook 11, 8 entries)
refl[12]: 3 bits (codebook 12, 8 entries)
refl[13]: 3 bits (codebook 13, 8 entries)
--- 52 bits total for reflection coefficients ---
Per subframe (x4):
pitch_gain: 5 bits (32 entries)
cb_index: 31 bits (combinatorial index into C(72,7))
exc_gain: 6 bits (64 entries)
pulse[0..6]: 3 bits each = 21 bits (7 pulses, 8 amplitudes each)
--- 63 bits per subframe, 252 total ---
combined_pitch: 24 bits (encodes 4 pitch lags)
--- total: 52 + 252 + 24 = 328 bits ---
The 24-bit combined pitch value encodes 4 subframe pitch lags:
p0_idx = combined % 151 (pitch range = 186-36+1 = 151)
remainder = combined / 151
delta1 = remainder % 48 (delta range = 48)
remainder = remainder / 48
delta2 = remainder % 48
delta3 = remainder (clamped to 0..47)
pitch[0] = p0_idx + 36
pitch[i] = base + delta[i-1] where base = max(36, min(prev - 23, 163))
The base calculation for delta decoding: half_delta = 48/2 - 1 = 23. If prev > 163 (upper_limit = 186 - 23), base = 163 - 23 = 140. If prev >= 59 (min_pitch + half_delta), base = prev - 23. Otherwise base = 36.
| Parameter | DS2 QP |
|---|---|
| Sample rate | 16000 Hz |
| Subframe size | 64 samples |
| Subframes/frame | 4 |
| Samples/frame | 256 |
| Reflection coeffs | 16 |
| Pitch range | 45–300 |
| Excitation pulses | 11 |
| Codebook | C(64,11) combinatorial |
| Frame bits | 448 |
| De-emphasis | y[n] = x[n] + 0.1*y[n-1] |
QP is simpler — no byte-swap. Strip 6-byte block headers, concatenate 506-byte payloads into a continuous bitstream. Frames are read sequentially from this stream (448 bits = 56 bytes each, but frames are not byte-aligned since the bitstream reader works at the bit level).
28 blocks = 14168 bytes of payload = 113344 bits. At 448 bits/frame, that's 253 frames per 28-block cycle.
refl[0]: 7 bits (codebook 0, 128 entries)
refl[1]: 7 bits (codebook 1, 128 entries)
refl[2]: 6 bits (codebook 2, 64 entries)
refl[3]: 6 bits (codebook 3, 64 entries)
refl[4]: 5 bits (codebook 4, 32 entries)
refl[5]: 5 bits (codebook 5, 32 entries)
refl[6]: 5 bits (codebook 6, 32 entries)
refl[7]: 5 bits (codebook 7, 32 entries)
refl[8]: 5 bits (codebook 8, 32 entries)
refl[9]: 4 bits (codebook 9, 16 entries)
refl[10]: 4 bits (codebook 10, 16 entries)
refl[11]: 4 bits (codebook 11, 16 entries)
refl[12]: 4 bits (codebook 12, 16 entries)
refl[13]: 3 bits (codebook 13, 8 entries)
refl[14]: 3 bits (codebook 14, 8 entries)
refl[15]: 3 bits (codebook 15, 8 entries)
--- 76 bits total ---
Per subframe (x4):
pitch: 8 bits (direct index, pitch = index + 45)
pitch_gain: 6 bits (64 entries)
cb_index: 40 bits (combinatorial index into C(64,11))
exc_gain: 6 bits (64 entries)
pulse[0..10]: 3 bits each = 33 bits (11 pulses)
--- 93 bits per subframe, 372 total ---
--- total: 76 + 372 = 448 bits ---
Note: QP encodes pitch directly per subframe (8 bits each), not as a combined value like SP.
Both codecs use the combinatorial number system to encode pulse positions. An index value selects k positions from {0..n-1}:
// Decode combinatorial index to k positions from {0..n-1}
// Positions are returned in DESCENDING order (do NOT sort!)
void decode_combinatorial(uint64_t index, int n, int k, int *positions) {
uint64_t remaining = index;
for (int i = k; i >= 1; i--) {
int v = i - 1;
while (v + 1 < n && comb(v + 1, i) <= remaining)
v++;
positions[k - i] = v;
remaining -= comb(v, i);
}
}- SP: C(72, 7), 31-bit index, max value 1,473,109,703
- QP: C(64, 11), 40-bit index, max value 743,595,781,823
excitation[i] = pitch_gain * adaptive_exc[i] + fixed_exc[i]
Where:
adaptive_exc= pitch memory repeated at the pitch periodfixed_exc= sparse pulse excitation: for each pulse position p[j],fixed_exc[p[j]] += pulse_amp[pulse_idx[j]] * exc_gain
When pitch < subframe_size, the adaptive excitation wraps: adaptive_exc[i] = pitch_memory[end - pitch + (i % pitch)].
Both codecs use an identical normalized lattice filter with the reflection coefficients:
// p = number of reflection coefficients (14 for SP, 16 for QP)
// coeffs[] = dequantized reflection coefficients
// state[] = persistent filter state (p elements, init to 0)
for (int n = 0; n < subframe_size; n++) {
double acc = excitation[n] - state[p-1] * coeffs[p-1];
for (int k = p-2; k >= 0; k--) {
acc -= state[k] * coeffs[k];
state[k+1] = coeffs[k] * acc + state[k];
}
state[0] = acc;
output[n] = acc;
}QP applies a first-order de-emphasis filter after synthesis:
y[n] = x[n] + 0.1 * y[n-1]
SP does not use de-emphasis.
All tables were extracted from the Olympus DssDecoder.dll / AudioSDK DLL via Ghidra. The complete tables (reflection coefficient codebooks + quantization tables) are available in the reference implementation. Summary of table sizes:
- Pitch gain: 32 entries (5-bit), linear range 0.05–2.0
- Excitation gain: 64 entries (6-bit), roughly exponential 0–5000
- Pulse amplitude: 8 entries (3-bit), symmetric [-0.952, 0.952]
- Reflection codebooks: 14 codebooks, sizes [32, 32, 16, 16, 16, 16, 16, 16, 8, 8, 8, 8, 8, 8] entries (f64)
- Pitch gain: 64 entries (6-bit), non-linear range 0.005–2.0
- Excitation gain: 64 entries (6-bit), range 3.9–4970.3
- Pulse amplitude: 8 entries (3-bit), asymmetric [-0.922, 0.931]
- Reflection codebooks: 16 codebooks, sizes [128, 128, 64, 64, 32, 32, 32, 32, 32, 16, 16, 16, 16, 8, 8, 8] entries (f64)
The full table data is in the source repo. The SP and QP codecs use completely different quantization tables — they're at different DLL addresses and have different value ranges.
-
Demuxer changes: The existing DSS demuxer (
libavformat/dss.c) needs to detect\x03ds2magic and handle the DS2 block structure. SP byte-swap logic is similar to DSS but not identical. QP needs a continuous-bitstream mode. The demuxer should set codec_id based on byte4 of the first block. -
New codec IDs: Need at least
AV_CODEC_ID_DS2_SPandAV_CODEC_ID_DS2_QP(or a singleAV_CODEC_ID_DS2with mode detection in init). -
Arithmetic precision: The DLL uses f64 (double) throughout. An f32 implementation would likely introduce audible artifacts given the lattice filter's sensitivity to coefficient precision.
-
State management: Each codec maintains persistent state across frames: lattice filter state (14 or 16 doubles), pitch memory buffer (up to 186+72 or 300+64 doubles), and de-emphasis state (QP only).
-
The DSS SP codec in FFmpeg is unrelated: Despite the similar name, FFmpeg's existing
dss_spcodec (11025 Hz, direct-form filter, different tables) cannot be adapted for DS2. The codec needs to be written from scratch.
The reference decoder has been verified against output from the proprietary Olympus DirectShow filters (via NCH Switch under Wine):
- DS2 SP: 1.0000 correlation, 99% sample-exact (±1 from f64→i16 rounding)
- DS2 QP: 1.0000 correlation, 100% sample-exact (bit-exact match)
Happy to provide test files and reference WAVs for anyone working on an FFmpeg implementation.