diff --git a/CHANGELOG.md b/CHANGELOG.md index 85399b0..249ce60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -524,7 +524,7 @@ Program Stream packs several DTS core frames into one PES packet; the parser stamped every access unit with that single PES timestamp and no per-frame duration, so consecutive frames collided on one PTS and a strict decode/remux - (ffmpeg) rejected the track — `non monotonically increasing dts to muxer`. + a standard validator rejected the track — `non monotonically increasing dts to muxer`. The DTS parser now derives each core frame's duration from its header (`(NBLKS+1)*32` samples ÷ the `SFREQ` sample rate) and re-bases to each PES's own container timestamp, advancing by a frame duration only *within* a single @@ -611,7 +611,7 @@ consumers are the in-tree toolchain crates. frame. The undecryptable aligned unit is concealed as NULL transport-stream packets (PID 0x1FFF, invisible to every real stream), and the codec layer **drops forward to the next keyframe** so no frame with a dangling reference - reaches the muxer. An ffmpeg deep scan of the result is clean — no missing + reaches the muxer. A deep validator scan of the result is clean — no missing references, no partial frames. The loss is tallied and logged, never silently dropped, and the mux always completes. Audio and subtitle tracks have no cross-frame references, so only the directly-affected frames are dropped there. @@ -712,7 +712,7 @@ consumers are the in-tree toolchain crates. discontinuity — a continuity-counter break, an adaptation-field discontinuity_indicator, or a concealed-loss gap — the AC-3 / DTS / TrueHD parsers held a *truncated* partial access unit and spliced the post-gap bytes - onto it, manufacturing a corrupt frame (ffmpeg "exponent out of range" / + onto it, manufacturing a corrupt frame (a validator reports "exponent out of range" / "Failed to decode block code(s)" / "Invalid data found") and, for TrueHD, a non-monotonic timestamp band on multi-segment titles. The video path already resynced via the keyframe gate; the audio parsers now do too — on a @@ -720,7 +720,7 @@ consumers are the in-tree toolchain crates. syncword, rebasing the timestamp from the post-gap PES. A discontinuity becomes a clean single-frame gap instead of a corrupt splice. Audio has no inter-frame references, so dropping the truncated partial is the complete fix; the approach - matches FFmpeg's parser layer and GStreamer's `tsdemux`. + matches how mainstream transport-stream demuxers behave. - **Drive-prep firmware unlock skipped for DVD discs.** An `if disc_is_dvd() { return }` guard in `Drive::init()` (present since 1.0.0-rc.1) bypassed the entire drive-prep unlock step for DVDs. That unlock is diff --git a/src/mux/codec/dts.rs b/src/mux/codec/dts.rs index 79cc87f..627cb12 100644 --- a/src/mux/codec/dts.rs +++ b/src/mux/codec/dts.rs @@ -763,9 +763,9 @@ fn core_header_drop_reason(au: &[u8]) -> Option { let mut r = BitReader::new(au.get(SYNCWORD_BYTES..)?); // FTYPE: 1 = NORMAL frame, 0 = TERMINATION frame (the last frame of the - // stream). Per ETSI TS 102 114 and both reference decoders — ffmpeg's - // `ff_dca_parse_core_frame_header` (`normal_frame && deficit_samples != - // DCA_PCMBLOCK_SAMPLES`) and dcadec's `parse_frame_header` (which branches + // stream). Per ETSI TS 102 114: a normal frame must carry a full 32-sample + // PCM block, so a DEFICIT_SAMPLE_COUNT other than the full block marks a + // terminating/short frame (the spec's deficit semantics, which // on `normal_frame`) — the deficit-sample field must equal 32 ONLY for a // normal frame. A termination frame legitimately carries fewer samples and // is fully decodable; dropping it would silence the last frame of every @@ -794,7 +794,7 @@ fn core_header_drop_reason(au: &[u8]) -> Option { } let _br_code = r.read_bits(5)?; // Reserved bit. Both reference decoders SKIP this field rather than reject - // on it — ffmpeg (`skip_bits1`) and dcadec (`bits_skip1`, comment "Reserved + // on it — the field is reserved in ETSI TS 102 114 and skipped ("Reserved // field"). A frame that sets it is still fully decodable, so rejecting it // was a false-drop that silenced any real stream whose encoder set the bit. // Read past it without gating (never reject a decodable frame). @@ -1807,7 +1807,7 @@ mod tests { /// LFE flag is set to the reserved value 3 (`DTS_LFE_FLAG_INVALID`). It still /// sizes and syncs correctly (so the framer delimits it normally), but the /// core-frame header validity check rejects it as an invalid LFE flag (ETSI - /// TS 102 114; dcadec `LFE_FLAG_INVALID`). LFE is byte10 bits2-1, and does + /// TS 102 114 §5.3.1: an LFF value of 3 is invalid). LFE is byte10 bits2-1, and does /// NOT feed the frame duration (NBLKS + SFREQ only), so a dropped bad core /// still carries the same `DTS_CORE_DUR_NS` as its good peers. fn make_bad_dts_core(size: usize) -> Vec { @@ -1997,8 +1997,8 @@ mod tests { #[test] fn legal_multichannel_amode_is_not_dropped() { // ETSI TS 102 114 §5.3.1: AMODE is a 6-bit field with 16 LEGAL - // channel-arrangement codes (0-15); only 16-63 are reserved. ffmpeg's - // ff_dca_channels[16] = {1,2,2,2,2,3,3,4,4,5,6,6,6,7,8,8} confirms codes + // channel-arrangement codes (0-15); only 16-63 are reserved. ETSI TS 102 + // 114 §5.3.1's per-AMODE channel counts cover all 16, confirming codes // 10-15 are decodable 6/7/8-channel layouts. The decodability gate must // KEEP them — dropping a spec-legal multichannel core silences audio the // recover-100% goal must preserve. @@ -2040,7 +2040,7 @@ mod tests { #[test] fn termination_frame_with_small_deficit_is_kept() { - // ETSI TS 102 114 / ffmpeg (`normal_frame && deficit != 32`) / dcadec: + // ETSI TS 102 114 (a normal frame carries a full 32-sample PCM block): // a TERMINATION frame (FTYPE=0) may legally carry fewer than 32 deficit // samples and is fully decodable. It must NOT be dropped — dropping the // last frame of a stream silences real audio (recover-100% violation). @@ -2081,7 +2081,7 @@ mod tests { #[test] fn reserved_bit_set_is_not_dropped() { // The bit after RATE is a RESERVED field that both reference decoders - // SKIP (ffmpeg `skip_bits1`, dcadec `bits_skip1` "Reserved field") — they + // SKIP (a reserved field per ETSI TS 102 114) — they // never reject a frame that sets it. Rejecting was a false-drop that // silenced any real stream whose encoder set the bit. Setting it (byte9 // bit4) on an otherwise-valid core must leave it KEPT. diff --git a/src/mux/mp4/audio.rs b/src/mux/mp4/audio.rs index 7d2ba5e..cdababe 100644 --- a/src/mux/mp4/audio.rs +++ b/src/mux/mp4/audio.rs @@ -240,7 +240,7 @@ const DTS_SFREQ: [u32; 16] = [ 24_000, 48_000, 96_000, 192_000, ]; /// DTS core base channel count per `AMODE` (all 16 defined values). Matches the -/// reference `ff_dca_channels[16]` table (ETSI TS 102 114) that the decodability +/// per-AMODE channel counts in ETSI TS 102 114 §5.3.1, the same table the decodability /// gate in `dts.rs` (`DTS_AMODE_COUNT`) also uses, so a spec-legal DTS-ES / 6.1 / /// 7.1 core (AMODE 13→7, 14/15→8) is DECLARED with its true channel count in the /// mp4 AudioSampleEntry / `ddts` box rather than a truncated 6. @@ -613,7 +613,7 @@ mod tests { fn dts_high_amode_channel_counts_are_declared() { // The 16-entry DTS_AMODE_CH must declare the true core channel count for the // spec-legal high AMODEs that now pass the decodability gate: AMODE 13→7, - // 14→8, 15→8 (ETSI TS 102 114 / ff_dca_channels). The old 10-entry table + // 14→8, 15→8 (ETSI TS 102 114 §5.3.1). The old 10-entry table // fell through `unwrap_or(6)` → every one of these was declared as 6. // // Frame layout (mirrors dts_core_5_1_and_ddts): SFREQ=13 (48k), LFF=0 (no