Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions Codecs/Codec.Dts/DtsCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ public sealed record DtsStreamInfo(
int SampleRate, int Channels, int Bitrate, int Amode, bool Lfe, long DurationSamples);

/// <summary>
/// Clean-room DTS Coherent Acoustics (DCA) core codec. The decoder is a faithful managed port of
/// the FFmpeg reference decoder (<c>libavcodec/dcadec.c</c> + <c>dcadata.c</c> +
/// <c>dcahuff.h</c>) and the encoder lives in the companion partial source file. The core decoder
/// emits interleaved little-endian signed 16-bit PCM at the stream's native channel count (the
/// AMODE full-bandwidth channels in document order, with the LFE channel last when present).
/// Clean-room DTS Coherent Acoustics (DCA) core codec; the encoder lives in the companion partial
/// source file. The core decoder emits interleaved little-endian signed 16-bit PCM at the stream's
/// native channel count, in the ITU/WAVE interleave order — front left, front right, front centre,
/// LFE, then the surrounds — rather than the centre-first AMODE order the bit stream uses.
/// <para>
/// Scope: only the standard 16-bit big-endian framing (sync 0x7FFE8001) is decoded; the 14-bit and
/// byte-swapped framings throw <see cref="NotSupportedException"/>. DTS-HD extension substreams
Expand Down Expand Up @@ -44,7 +43,7 @@ public static DtsStreamInfo ReadStreamInfo(Stream input) {

/// <summary>
/// Decodes a DTS stream into raw interleaved little-endian signed 16-bit PCM on
/// <paramref name="output"/>. Channels are emitted in AMODE document order with LFE last.
/// <paramref name="output"/>. Channels are emitted in the ITU/WAVE interleave order.
/// Throws <see cref="NotSupportedException"/> for the unsupported 14-bit / LE framings.
/// </summary>
public static void Decompress(Stream input, Stream output) {
Expand Down
103 changes: 68 additions & 35 deletions Codecs/Codec.Dts/DtsFrameDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Codec.Dts;
/// <summary>
/// Decodes a single DTS Coherent Acoustics (DCA) core frame to per-channel float PCM. One instance
/// is reused across the stream so the QMF synthesis memory and the ADPCM predictor history persist
/// between frames. The decode follows the DTS core bitstream / FFmpeg's <c>dcadec.c</c>:
/// between frames. The decode follows the DTS core bitstream:
/// the primary audio coding header (subband activity, VQ start, joint intensity, the bit-allocation
/// / scale-factor / transient / quantization-index code-book selections and scale-factor adjusts),
/// then per subframe a set of sub-subframes carrying bit allocation, scale factors, the quantized
Expand All @@ -23,6 +23,8 @@ internal sealed class DtsFrameDecoder {
private const int MaxPrimChannels = 7; // DCA_PRIM_CHANNELS_MAX
private const int Subbands = 32; // DCA_SUBBANDS
private const int MaxSubsubframes = 4;
private const int MaxAbits = 32; // DCA_ABITS_MAX: bit-allocation index range
private const int CodeBooks = 10; // bit-allocation indices that can carry a sample code book

// ── Code books (built once) ───────────────────────────────────────────────
private static readonly DtsBitAllocBook BitAllocIndex = BuildBitAllocIndex();
Expand Down Expand Up @@ -62,15 +64,19 @@ public DtsFrameDecoder() {
private int _subframes;
private int _bitRateIndex;
private bool _perfectReconstruction;
private int[] _channelMap = [];

private readonly int[] _subbandActivity = new int[MaxPrimChannels];
private readonly int[] _vqStartSubband = new int[MaxPrimChannels];
private readonly int[] _jointIntensity = new int[MaxPrimChannels];
private readonly int[] _transientHuffman = new int[MaxPrimChannels];
private readonly int[] _scalefactorHuffman = new int[MaxPrimChannels];
private readonly int[] _bitallocHuffman = new int[MaxPrimChannels];
private readonly int[][] _quantIndexHuffman = NewIntMatrix(MaxPrimChannels, 11);
private readonly float[][] _scalefactorAdj = NewFloatMatrix(MaxPrimChannels, 11);
// Indexed by the bit-allocation index, which the subframe header allows up to 26 even though only
// groups 1..10 carry a transmitted code-book selector and scale-factor adjustment. Sizing these to
// the group count alone puts every wide-band subband out of bounds.
private readonly int[][] _quantIndexHuffman = NewIntMatrix(MaxPrimChannels, MaxAbits);
private readonly float[][] _scalefactorAdj = NewFloatMatrix(MaxPrimChannels, MaxAbits);

// Per-subframe header state.
private readonly int[][] _predictionMode = NewIntMatrix(MaxPrimChannels, Subbands);
Expand All @@ -85,13 +91,15 @@ public DtsFrameDecoder() {
// LFE decimated samples (history + current frame). Sized for the worst case.
private readonly float[] _lfeData = new float[64 * 2 + 256];
private float _lfeScaleFactor;
private int _lfeWritten;
private int _lfeHistoryLfe = -1;

// Subband samples for the whole frame: [block][chan][subband][8].
private float[][][][]? _subbandSamples;

/// <summary>
/// Decodes the core frame at <paramref name="offset"/>. On success <paramref name="outChannelCount"/>
/// is the native channel count (AMODE channels + LFE last when present) and the return value is
/// is the native channel count and the return value is, in ITU/WAVE interleave order,
/// <c>[channel][sample]</c> float PCM (normalised to roughly ±1). Returns <see langword="null"/>
/// when the frame cannot be decoded.
/// </summary>
Expand All @@ -112,6 +120,7 @@ public DtsFrameDecoder() {
// MULTIRATE_INTER selects the perfect-reconstruction prototype; it is bit 0 of the field that
// immediately follows the (optional) header CRC. Re-read it from the parsed header position.
this._perfectReconstruction = ReadMultirateInter(data, offset, header);
this._channelMap = DtsFrameHeader.ChannelMap(header.Amode, header.Lfe > 0);

// Seek the reader to the end of the parsed frame header (which is byte-exact in bits).
r.SkipBits(header.HeaderBitLength);
Expand All @@ -120,12 +129,19 @@ public DtsFrameDecoder() {
return null;

var totalChannels = this._primChannels + (this._lfe > 0 ? 1 : 0);
// The audio coding header carries its own channel count; when it disagrees with AMODE the
// arrangement is unknown, so fall back to bit-stream order rather than shuffle blindly.
if (this._channelMap.Length != totalChannels) {
this._channelMap = new int[totalChannels];
for (var c = 0; c < totalChannels; ++c)
this._channelMap[c] = c;
}
var blocks = this._sampleBlocks / 8;
if (blocks <= 0)
return null;

this._subbandSamples = NewSubbandSamples(blocks, this._primChannels);
Array.Clear(this._lfeData, 0, this._lfeData.Length);
this.CarryLfeHistory(blocks);

var currentSubframe = 0;
var currentSubsubframe = 0;
Expand Down Expand Up @@ -163,6 +179,20 @@ public DtsFrameDecoder() {
}
}

// The interpolation FIR reaches back past the first decimated sample of the frame, so the tail of
// the previous frame has to stay in place; clearing the whole buffer restarts the filter from
// silence once per frame and leaves a periodic artefact on the LFE channel.
private void CarryLfeHistory(int blocks) {
var history = 8 * this._lfe;
if (this._lfeHistoryLfe == this._lfe && this._lfeWritten >= history)
Array.Copy(this._lfeData, this._lfeWritten - history, this._lfeData, 0, history);
else
Array.Clear(this._lfeData, 0, Math.Max(history, 0));
Array.Clear(this._lfeData, history, this._lfeData.Length - history);
this._lfeWritten = history + 2 * this._lfe * blocks;
this._lfeHistoryLfe = this._lfe;
}

// The MULTIRATE_INTER flag sits right after the optional 16-bit header CRC. Re-parse minimally.
private static bool ReadMultirateInter(byte[] data, int offset, DtsFrameHeader header) {
var buffer = data.AsSpan(offset, Math.Min(data.Length - offset, 32)).ToArray();
Expand Down Expand Up @@ -196,14 +226,13 @@ private bool ParseAudioCodingHeader(DtsBitReader r) {
for (var i = 0; i < this._primChannels; ++i) this._bitallocHuffman[i] = (int)r.ReadBits(3);

for (var i = 0; i < this._primChannels; ++i)
this._quantIndexHuffman[i][0] = 0;
Array.Clear(this._quantIndexHuffman[i]);
for (var j = 1; j < 11; ++j)
for (var i = 0; i < this._primChannels; ++i)
this._quantIndexHuffman[i][j] = (int)r.ReadBits(QuantIndexBitLen[j]);

for (var j = 0; j < 11; ++j)
for (var i = 0; i < this._primChannels; ++i)
this._scalefactorAdj[i][j] = 1f;
for (var i = 0; i < this._primChannels; ++i)
Array.Fill(this._scalefactorAdj[i], 1f);
for (var j = 1; j < 11; ++j)
for (var i = 0; i < this._primChannels; ++i)
if (this._quantIndexHuffman[i][j] < QuantIndexThreshold[j])
Expand Down Expand Up @@ -257,7 +286,7 @@ private bool ParseSubframeHeader(DtsBitReader r, int blockIndex, int currentSubf
for (var j = 0; j < this._primChannels; ++j) {
uint[] scaleTable;
int logSize;
if (this._scalefactorHuffman[j] == 6) { scaleTable = DtsTables.ScaleFactorQuant7; logSize = 7; }
if (this._scalefactorHuffman[j] > 5) { scaleTable = DtsTables.ScaleFactorQuant7; logSize = 7; }
else { scaleTable = DtsTables.ScaleFactorQuant6; logSize = 6; }

for (var k = 0; k < this._subbandActivity[j]; ++k) {
Expand Down Expand Up @@ -355,25 +384,30 @@ private bool DecodeSubsubframe(DtsBitReader r, int blockIndex, int currentSubfra
rscale[l] = 0f;
Array.Clear(block, 8 * l, 8);
} else {
// A code book is only in play when the transmitted selector falls inside that group's
// size; otherwise the samples are block codes (abits <= 7) or plain fixed-width values.
var book = abits <= CodeBooks ? SampleBitAlloc[abits] : null;
var huffman = book != null && sel < QuantIndexThreshold[abits] && book.Vlc[sel] != null;

var sfi = this._transitionMode[k][l] != 0 && subsubframe >= this._transitionMode[k][l] ? 1 : 0;
rscale[l] = quantStepSize * this._scaleFactor[k][l][sfi] * this._scalefactorAdj[k][sel];

var book = abits < SampleBitAlloc.Length ? SampleBitAlloc[abits] : null;
if (abits >= 11 || book == null || book.Vlc[sel] == null) {
if (abits <= 7) {
var size = DtsBlockCode.Sizes[abits - 1];
var levels = DtsBlockCode.Levels[abits - 1];
var code1 = (int)r.ReadBits(size);
var code2 = (int)r.ReadBits(size);
if (DtsBlockCode.DecodeBlockCodes(code1, code2, levels, block, 8 * l) != 0)
return false;
} else {
for (var m = 0; m < 8; ++m)
block[8 * l + m] = r.ReadSigned(abits - 3);
}
// The scale-factor adjustment belongs to the bit-allocation index, and it applies only
// when the samples were Huffman coded.
var adj = huffman ? this._scalefactorAdj[k][abits] : 1f;
rscale[l] = quantStepSize * this._scaleFactor[k][l][sfi] * adj;

if (huffman) {
for (var m = 0; m < 8; ++m)
block[8 * l + m] = book!.Get(r, sel);
} else if (abits <= 7) {
var size = DtsBlockCode.Sizes[abits - 1];
var levels = DtsBlockCode.Levels[abits - 1];
var code1 = (int)r.ReadBits(size);
var code2 = (int)r.ReadBits(size);
if (DtsBlockCode.DecodeBlockCodes(code1, code2, levels, block, 8 * l) != 0)
return false;
} else {
for (var m = 0; m < 8; ++m)
block[8 * l + m] = book.Get(r, sel);
block[8 * l + m] = r.ReadSigned(abits - 3);
}
}
}
Expand Down Expand Up @@ -432,21 +466,20 @@ private bool DecodeSubsubframe(DtsBitReader r, int blockIndex, int currentSubfra

// ── QMF synthesis + LFE interpolation → per-channel PCM ───────────────────
private void FilterChannels(int blocks, float[][] pcm) {
var totalChannels = this._primChannels + (this._lfe > 0 ? 1 : 0);
// Output ordering: decoded prim channels in document (AMODE) order, LFE last. The QMF scale
// 1/sqrt(2) / 32768 matches the reference; we keep ±1-normalised floats (no /32768) for WAV.
var qmfScale = (float)(1.0 / Math.Sqrt(2.0));
// The QMF output is scaled to the +/-1 float domain the caller quantizes from; the reference
// filterbank gain is 1/sqrt(2) over a 15-bit sample range.
var qmfScale = (float)(1.0 / Math.Sqrt(2.0) / 32768.0);
var map = this._channelMap;

for (var blk = 0; blk < blocks; ++blk) {
var subbandSamples = this._subbandSamples![blk];
for (var k = 0; k < this._primChannels; ++k)
this._qmf[k].Process(subbandSamples[k], this._subbandActivity[k], pcm[k], blk * 256,
this._qmf[k].Process(subbandSamples[k], this._subbandActivity[k], pcm[map[k]], blk * 256,
this._perfectReconstruction, qmfScale);

if (this._lfe > 0) {
var lfeChannel = totalChannels - 1;
DtsLfe.Interpolate(this._lfe, this._lfeData, 2 * this._lfe * (blk + 4), pcm[lfeChannel], blk * 256);
}
if (this._lfe > 0)
DtsLfe.Interpolate(this._lfe, this._lfeData, 2 * this._lfe * (blk + 4),
pcm[map[this._primChannels]], blk * 256);
}
}

Expand Down
29 changes: 29 additions & 0 deletions Codecs/Codec.Dts/DtsFrameHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ public readonly record struct DtsFrameHeader(
_ => $"user-defined ({amode})",
};

/// <summary>
/// Maps a channel's position in the bit stream to its position in the decoded interleaved PCM.
/// The DTS core transmits the front channels centre-first (C, L, R) and carries the LFE channel
/// last, while interleaved PCM uses the ITU/WAVE order — front left, front right, front centre,
/// LFE, then the surrounds. The returned array is indexed by bit-stream channel (AMODE channels in
/// document order, LFE appended) and yields the interleave slot. AMODE codes above 9 describe
/// arrangements the core decoder does not reconstruct, and keep the bit-stream order.
/// </summary>
internal static int[] ChannelMap(int amode, bool lfe) => (amode, lfe) switch {
(0, false) => [0], (0, true) => [0, 1],
(1, false) => [0, 1], (1, true) => [0, 1, 2],
(2, false) => [0, 1], (2, true) => [0, 1, 2],
(3, false) => [0, 1], (3, true) => [0, 1, 2],
(4, false) => [0, 1], (4, true) => [0, 1, 2],
(5, false) => [2, 0, 1], (5, true) => [2, 0, 1, 3],
(6, false) => [0, 1, 2], (6, true) => [0, 1, 3, 2],
(7, false) => [2, 0, 1, 3], (7, true) => [2, 0, 1, 4, 3],
(8, false) => [0, 1, 2, 3], (8, true) => [0, 1, 3, 4, 2],
(9, false) => [2, 0, 1, 3, 4], (9, true) => [2, 0, 1, 4, 5, 3],
_ => Identity(AmodeChannelCount(amode) + (lfe ? 1 : 0)),
};

private static int[] Identity(int count) {
var map = new int[count];
for (var i = 0; i < count; ++i)
map[i] = i;
return map;
}

/// <summary>Channel count implied by an AMODE code (excluding the LFE channel).</summary>
public static int AmodeChannelCount(int amode) => amode switch {
0 => 1, 1 => 2, 2 => 2, 3 => 2, 4 => 2, 5 => 3, 6 => 3, 7 => 4,
Expand Down
26 changes: 15 additions & 11 deletions Codecs/Codec.Dts/DtsQmf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ namespace Codec.Dts;

/// <summary>
/// 32-band cosine-modulated QMF synthesis filterbank for the DCA core, reconstructing 256 PCM
/// samples per channel per block from 8 sub-subframe vectors of 32 subband samples each. This is a
/// faithful port of FFmpeg's <c>dca_qmf_32_subbands</c> + <c>synth_filter_float</c>
/// (<c>libavcodec/dcadsp.c</c>, <c>synth_filter.c</c>): the per-subband sign flip
/// <c>((i-1)&amp;2)</c>, a direct (matrix-multiply) 64→32 <c>imdct_half</c>, and the 512-tap
/// polyphase window/overlap stage driven by the perfect- or non-perfect-reconstruction prototype
/// (<see cref="DtsTables.Fir32Perfect"/> / <see cref="DtsTables.Fir32NonPerfect"/>). The direct
/// IMDCT replaces FFmpeg's FFT path; the task permits a direct matrix multiply for the transform.
/// samples per channel per block from 8 sub-subframe vectors of 32 subband samples each. The stage
/// is the per-subband sign flip <c>((i-1)&amp;2)</c>, the second half of a 64-point IMDCT over the
/// 32 subband values, and a 512-tap polyphase window/overlap driven by the perfect- or
/// non-perfect-reconstruction prototype
/// (<see cref="DtsTables.Fir32Perfect"/> / <see cref="DtsTables.Fir32NonPerfect"/>). The IMDCT is
/// evaluated as a direct matrix multiply, which is exact and cheap enough at this size.
/// </summary>
public sealed class DtsQmf {

Expand All @@ -20,17 +19,22 @@ public sealed class DtsQmf {
private readonly float[] _synthBuf2 = new float[32];
private int _synthBufOffset;

// Pre-computed imdct_half cosine matrix: out[k] = sum_n in[n] * Cos[k][n], k,n in 0..31.
// FFmpeg's MDCT-half of size 64 produces 32 outputs; the equivalent direct kernel is
// cos(pi/64 * (2k+1) * (n + 0.5)) scaled to match the synth_filter window convention.
// Pre-computed half-IMDCT cosine matrix: out[k] = sum_n in[n] * Cos[k][n], k,n in 0..31.
//
// The filterbank stage is the second half of a 64-point IMDCT over 32 coefficients:
// full[i] = -sum_n in[n] * cos(pi * (2i + 1 + 32) * (2n + 1) / 128), i = 0..63
// of which the synthesis window consumes full[16..47]. Substituting i = k + 16 leaves
// out[k] = -sum_n in[n] * cos(pi * (2k + 65) * (2n + 1) / 128).
// The phase term is what places each subband at its own centre frequency; getting it wrong
// still yields a full-amplitude signal, just one built from the wrong modulation images.
private static readonly float[][] ImdctCos = BuildImdctCos();

private static float[][] BuildImdctCos() {
var m = new float[32][];
for (var k = 0; k < 32; ++k) {
m[k] = new float[32];
for (var n = 0; n < 32; ++n)
m[k][n] = (float)Math.Cos(Math.PI / 64.0 * (2 * k + 1) * (2 * n + 1));
m[k][n] = (float)-Math.Cos(Math.PI * (2 * k + 65) * (2 * n + 1) / 128.0);
}
return m;
}
Expand Down
Loading
Loading