Skip to content

Fix DVD audio compatibility gaps: bitrate, DTS, and track order - #137

Merged
ScottMorris merged 7 commits into
mainfrom
fix/audio-dvd-compat
Aug 21, 2026
Merged

ScottMorris merged 7 commits into
mainfrom
fix/audio-dvd-compat

Conversation

@ScottMorris

Copy link
Copy Markdown
Contributor

Summary

Four independent fixes to DVD-Video audio compatibility handling, each a self-contained commit:

  • Wrong default audio track: dvdauthor's own auto-detection of physical audio-track order doesn't reliably match the order streams actually appear in a vob — confirmed in practice with a mixed AC3+MP2 title, where dvdauthor placed the non-default MP2 stream at physical audio 0. Now declares every title's audio streams explicitly, in authored order.
  • Audio bitrate ceiling: a stream can be a DVD-legal codec by name and still be illegal to copy because its bitrate is over the DVD-Video ceiling for that codec (e.g. 640 kbps AC-3 — legal AC-3, not legal DVD-Video). Adds a separate bitrate compatibility check, consulted alongside codec before defaulting a track to copy mode.
  • DTS re-encoding: ffmpeg's only DTS encoder path is confirmed broken — it corrupts once muxed through the DVD program-stream muxer (real dvdauthor output showed hundreds of "audio sector out of range" warnings, with playback silently falling back to a different track). Refused at build time, validation time, and in the Titles UI dropdown; DTS stays legal only as a stream copy.
  • Video copy-compatibility: the video source-stream picker defaulted to copy mode unconditionally, without checking resolution or frame rate the way the audio side already did.

Test plan

  • pnpm validate passes clean (format, JS tests, TS build, cargo fmt/clippy -D warnings, cargo nextest) — 230/230 Rust tests passing, including 12 new tests covering all four fixes
  • Verified the audio-track-order fix against a real built disc this session: lsdvd confirms AC3 (declared default) lands at physical stream 0/0x80 and stays there across rebuilds, matching authored order
  • Manual spot-check in the Titles UI: import a source with an over-ceiling-bitrate AC3 track and confirm it defaults to re-encode, not copy
  • Manual spot-check: attempt a DTS re-encode mapping in the UI and confirm the dropdown disables it with the explanatory tooltip

@ScottMorris ScottMorris added bug Something isn't working backend Rust backend, commands, and native runtime behaviour frontend Frontend UI, styling, and webview behaviour assets Media import, inspection, and asset management work build-pipeline Authoring, encoding, packaging, and export pipeline work labels Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Spindle JS tests

391 tests  ±0   391 ✅ ±0   7s ⏱️ +3s
 36 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit fcae297. ± Comparison against base commit 3369541.

♻️ This comment has been updated with latest results.

@ScottMorris

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Spindle Rust tests

354 tests  +10   354 ✅ +10   3s ⏱️ -1s
  2 suites ± 0     0 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit fcae297. ± Comparison against base commit 3369541.

♻️ This comment has been updated with latest results.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6f0018f35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +43 to +45
let mapping = titleset.titles.iter().find_map(|t| t.audio_mappings.get(i));
if let Some(am) = mapping {
let format = dvdauthor_audio_format(am.output_target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive copied-audio declarations from the source codec

When a mapping uses copy mode, output_target does not determine the emitted codec: build_ffmpeg_transcode_command passes the source stream through unchanged. The UI nevertheless allows, for example, selecting DTS or MP2 while copying an AC3 source, and this new code then declares that physical AC3 stream to dvdauthor using the selected target format. That mismatch can cause dvdauthor to reject or incorrectly author the audio stream; resolve the actual source codec for copied mappings or reject mismatched copy targets before generating the declaration.

Useful? React with 👍 / 👎.

Comment on lines +533 to +544
let compatible = a.bitrate_bps.is_some_and(|bps| bps <= ceiling);
PropertyCheck {
value: a
.bitrate_bps
.map_or("unknown".to_string(), |bps| format!("{}kbps", bps / 1000)),
dvd_requires: format!("at most {}kbps for {}", ceiling / 1000, a.codec),
action: if compatible {
"none".to_string()
} else {
"re-encode".to_string()
},
compatible,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include bitrate failures in the overall compatibility result

For an otherwise DVD-compatible MPEG-2 asset containing 640 kbps AC3, this new check correctly marks the bitrate incompatible, but assess_dvd_compatibility still classifies the asset as RemuxCompatible because it only checks the audio codec name. The Assets page uses that unchanged overall value for its badge and only renders each audio codec check, so users are told the asset is remux-compatible even though selecting it now forces an audio re-encode. Fold this bitrate result into the overall assessment and expose it in the compatibility details.

Useful? React with 👍 / 👎.

Comment on lines +839 to +844
<option
value="DTS"
disabled={am.copyMode !== 'copy'}
title="ffmpeg has no reliable DTS encoder — DTS is only supported as a passthrough of an already-DTS source, not as a re-encode target."
>
DTS{am.copyMode !== 'copy' ? ' (passthrough only)' : ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the new audio compatibility behaviour

This changes user-facing behaviour by making DTS passthrough-only and automatically switching incompatible audio and video streams to re-encode, but the commit updates neither README.md nor SPEC.md. Document the new restrictions and automatic defaults in both required files so the supported authoring workflow matches the application.

AGENTS.md reference: AGENTS.md:L89-L92

Useful? React with 👍 / 👎.

@ScottMorris

Copy link
Copy Markdown
Contributor Author

All three findings were real, verified, and addressed in the latest commits:

  • P1 (copy-mode audio declared with the wrong codec) — confirmed reachable via the UI (copyMode and outputTarget are independently settable, and switching to copy doesn't reset a stale outputTarget). Fixed in 9551315: declared_audio_format now resolves the real source codec via the title's asset for copy-mode mappings, falling back to output_target only when the source can't be resolved. Added a regression test (dvdauthor_xml_declares_copied_audio_using_the_actual_source_codec) covering the exact scenario described.
  • P2 (bitrate failures not reflected in the overall compatibility result) — confirmed: assess_dvd_compatibility only checked codec name. Fixed in d9c578f by extracting shared audio_bitrate_ceiling_bps/audio_bitrate_is_dvd_legal helpers used by both the overall assessment and the per-stream detail, so they can't drift apart again. Added a regression test asserting a 640 kbps AC3 asset is no longer reported RemuxCompatible.
  • P1 (missing README/SPEC updates) — addressed in 7dca4a8: documents the confirmed-broken DTS encoder, the bitrate-ceiling copy-eligibility rule, updates the SPEC's DTS error example to the actual message, and narrows the related open question.

Full pnpm validate passes clean (232/232 Rust tests, including 3 new tests from this round).

@ScottMorris

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dca4a8ae7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +46 to +49
let mapping = titleset
.titles
.iter()
.find_map(|t| t.audio_mappings.get(i).map(|am| (t, am)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject inconsistent codecs for a titleset audio slot

When titles in the same titleset produce different codecs at the same audio index—for example, title 1 emits AC3 at index 0 while title 2 emits MP2—this find_map uses only title 1 to create the titleset-level <audio> declaration, even though ffmpeg independently encodes title 2 as MP2. The declaration therefore disagrees with title 2's physical stream, which can make dvdauthor reject or mis-author that title; validate/normalise the codec for each slot across every title before emitting a single declaration.

Useful? React with 👍 / 👎.

a.codec.as_str(),
"ac3" | "dts" | "pcm_s16le" | "pcm_s16be" | "mp2" | "lpcm"
)
) && audio_bitrate_is_dvd_legal(&a.codec, a.bitrate_bps)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require all audio streams to pass the remux check

For a multi-audio asset containing one compliant stream and one over-ceiling stream, the surrounding .any(...) makes this new bitrate predicate succeed on the compliant stream and still reports RemuxCompatible, although TitleEditor includes every stream and re-encodes the incompatible one. Fresh evidence after the prior review is that the current fix added the bitrate predicate inside .any rather than requiring every included audio stream to pass, so the Assets badge remains inconsistent for mixed assets.

Useful? React with 👍 / 👎.

},
compatible: is_dvd_audio,
},
bitrate: bitrate_check,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render the new bitrate check in asset details

This stores the new incompatibility reason, but CompatibilityDetailView in apps/spindle/src/pages/AssetsPage.tsx lines 450-452 still adds only a.codec to its rows, so users who expand an over-ceiling AC3 asset cannot see why it requires re-encoding. Fresh evidence after the prior review is that the model and inspection result now contain bitrate, while the current Assets view still never consumes it.

Useful? React with 👍 / 👎.

Without explicit `<audio>` declarations, dvdauthor auto-detects a
title's audio streams from the vob itself, and its inferred physical-
track order does not reliably match the order the streams actually
appear in. Confirmed in practice with a mixed AC3+MP2 title: dvdauthor
placed the second (MP2) stream at physical audio 0, silently ignoring
which track was authored as the default — so players default to the
wrong (non-default) audio track regardless of the disc's own intent.

Fix: declare every title's audio streams explicitly, in
`audio_mappings` order, so dvdauthor's physical-track assignment
matches what was authored instead of whatever it infers from the vob.

`dvdauthor_subpicture_language` is renamed to `dvdauthor_language_code`
since it's now shared between subpicture and audio stream declarations
rather than subpicture-only.
A source stream can be a DVD-legal codec by name and still be illegal
to stream-copy because its bitrate exceeds what DVD-Video allows for
that codec — e.g. 640 kbps AC-3 is a legal AC-3 bitstream (produced by
plenty of non-DVD encoders) but exceeds the DVD-Video spec's 448 kbps
AC-3 ceiling. The codec-name check alone was greenlighting these for a
stream copy, which would have shipped an out-of-spec disc.

Adds a `bitrate` compatibility check to `AudioStreamCompatibility`,
separate from `codec`, covering AC3 (448 kbps), MP2 (384 kbps), and
DTS (1536 kbps, rounded up slightly from the DVD spec's 1509.75 kbps
for encoder/muxer bitrate-field rounding). An unknown bitrate on a
ceiling-bearing codec defaults to incompatible rather than
optimistically assuming it's fine. Codecs without a simple bitrate
ceiling (LPCM, whose legality depends on channel count/sample
rate/depth) are unaffected.

The Titles UI now requires both `codec.compatible` and
`bitrate.compatible` before defaulting a track to copy mode, in both
the initial auto-map and the existing-mapping compatibility check.
ffmpeg's only DTS encoder path (`-c:a dts`, reachable only behind
`-strict -2` since it's marked experimental) is confirmed broken, not
merely unsupported in theory: it produces a stream that decodes fine
in isolation but corrupts once actually muxed through the `-f dvd`
program-stream muxer. Real dvdauthor output on a DTS-mapped title
showed hundreds of "audio sector out of range" warnings, and playback
silently fell back to a different audio stream.

DTS is DVD-legal and works fine as a stream copy of an already-DTS
source; re-encoding *to* DTS is not something this toolchain can do
reliably, so it's refused outright rather than shipped broken:

- `build_ffmpeg_transcode_command` now returns `crate::Result<...>`
  and errors out on a DTS re-encode mapping, instead of emitting a
  command that produces a corrupt disc.
- Validation flags a DTS re-encode mapping as an error at project-edit
  time too, so the problem surfaces before a build is even attempted.
- The Titles UI disables DTS as a re-encode target in the output
  dropdown (offering it only for copy-mode tracks, with an explanatory
  tooltip), and falls back the output target to AC3 if a track already
  set to DTS is switched to re-encode mode.
…o to copy mode

Picking a video source stream in the Titles editor defaulted it to
copy mode unconditionally, even when its resolution or frame rate
weren't actually DVD-legal — only the audio side had a compatibility
gate. Since `CompatibilityDetail.video` always assesses whichever
stream inspection treated as primary (in practice `videoStreams[0]`),
picking any other stream also has no compatibility signal to check
against and must default to re-encode rather than assume copy.

Adds `isVideoCopyCompatible()`, checking codec, resolution, and frame
rate together (any one mismatch means re-encode), and uses it both in
the initial auto-map and when the user changes the video source stream.
Follow-up to the audio-stream-declaration-order fix, from Codex
review: `output_target` only reflects what a *re-encoded* stream
becomes — `build_ffmpeg_transcode_command` emits `-c:a copy` for a
`CopyMode::Copy` mapping, ignoring `output_target` entirely and
passing the source stream's own bytes through unchanged. The Titles
UI lets `copyMode` and `outputTarget` be set independently, though, so
a track can end up in copy mode with a stale `output_target` left over
from before the user switched modes (e.g. was re-encoding AC3 -> MP2,
then switched to copy). Declaring that stale target's format to
dvdauthor would tell it a physical AC3 stream is MP2, which can make
dvdauthor reject or mis-author the stream.

`declared_audio_format` now resolves the real source codec (via the
title's `source_asset_id` and the mapping's `source_stream_index`) for
copy-mode mappings, falling back to `output_target` only when the
source asset/stream can't be resolved.
…ility result

Follow-up to the audio bitrate ceiling check, from Codex review: the
new per-stream `bitrate` check landed in `build_compatibility_detail`,
but `assess_dvd_compatibility` (which sets the asset's overall
`RemuxCompatible`/`TransformCompatible`/`ReEncodeRequired` label) still
only checked audio codec *name*. An otherwise DVD-compatible MPEG-2
asset with 640 kbps AC3 was still reported `RemuxCompatible` overall
even though selecting it forces an audio re-encode — the Assets page
badge told users the file was fine to remux when it wasn't.

Extracts the ceiling table and the compatibility check into shared
`audio_bitrate_ceiling_bps`/`audio_bitrate_is_dvd_legal` helpers, used
by both `assess_dvd_compatibility`'s overall `has_dvd_audio` check and
`build_compatibility_detail`'s per-stream `bitrate` check, so the two
can no longer drift apart.
Per AGENTS.md's docs-sync convention, from Codex review: this PR's
DTS re-encode refusal and audio bitrate ceiling check are both
user-facing behaviour changes that weren't reflected in README.md or
SPEC.md.

- README: notes that compatibility explanations now cover per-codec
  DVD-Video audio bitrate ceilings, not just codec-name matching.
- SPEC §12.2.4: documents that DTS output is confirmed unavailable as
  a re-encode target regardless of environment (not merely "not yet
  probed"), and that copy eligibility requires both codec name and
  bitrate to be DVD-legal.
- SPEC §21.2: updates the DTS error example to the actual message the
  app now emits.
- SPEC §26 open question 9: narrowed now that DTS re-encoding isn't
  offered at all — the remaining question is only about copy-mode DTS
  passthrough, which was never gated by the broken encoder.
@ScottMorris
ScottMorris force-pushed the fix/audio-dvd-compat branch from 7dca4a8 to fcae297 Compare August 21, 2026 04:54
@ScottMorris

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fcae297f0f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +839 to +842
<option
value="DTS"
disabled={am.copyMode !== 'copy'}
title="ffmpeg has no reliable DTS encoder — DTS is only supported as a passthrough of an already-DTS source, not as a re-encode target."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset DTS before implicit re-encoding

When an already-DTS track is in copy mode, selecting a channel layout or bitrate still changes copyMode to re-encode without changing outputTarget. Disabling DTS only in this codec picker does not cover those implicit transitions, so the new backend refusal makes the resulting project fail validation and build. Apply the same DTS-to-AC3 fallback to those controls, or disable them while DTS passthrough is selected.

Useful? React with 👍 / 👎.

Comment on lines +50 to +52
if let Some((title, am)) = mapping {
let format = declared_audio_format(am, title, assets);
let lang = dvdauthor_language_code(&am.language);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate titleset audio languages before declaring them

When titles in one titleset assign different languages to the same audio slot—for example, English on title 1 and French on title 2—this titleset-level declaration takes only the first title's language and applies it to the slot for every title. The second title is therefore authored with incorrect language metadata despite the project model allowing per-title values; require a consistent language per slot or normalise the mappings before emitting the declaration.

Useful? React with 👍 / 👎.

@ScottMorris
ScottMorris merged commit 3b02ccf into main Aug 21, 2026
8 checks passed
@ScottMorris
ScottMorris deleted the fix/audio-dvd-compat branch August 21, 2026 04:59
ScottMorris added a commit that referenced this pull request Aug 21, 2026
…gaps (#138)

## Summary
- Two follow-up fixes from a post-merge Codex review on #137, both real
regressions introduced in that PR:
- Titles that share a titleset but declare different audio codec or
language at the same slot index now fail validation with a clear error,
instead of the authored dvdauthor XML silently matching only the first
title (DVD-Video's VTSI audio attribute table is shared across an entire
titleset, not per-title).
- Picking a channel layout or bitrate on a DTS copy-mode track now falls
back `outputTarget` to AC3, matching the fallback the copy-mode selector
already had — previously this combination flipped to re-encode+DTS,
which fails validation and the build.

## Test plan
- [x] `pnpm validate` (format:check, vitest, tsc build, cargo fmt/clippy
-D warnings, cargo nextest) — all green
- [x] New Rust tests:
`validate_titles_flags_inconsistent_codec_across_titles_in_a_titleset`,
`validate_titles_flags_inconsistent_language_across_titles_in_a_titleset`,
`validate_titles_does_not_flag_consistent_titleset_audio`
- [x] New JS tests: DTS→AC3 fallback via the bitrate control and the
channel-layout control
@ScottMorris ScottMorris mentioned this pull request Aug 21, 2026
2 tasks done
ScottMorris added a commit that referenced this pull request Aug 21, 2026
## Summary
- Prepare the v0.4.0 release: bump all release-facing manifest versions
(root and desktop `package.json`, `tauri.conf.json`, both Cargo
manifests, `Cargo.lock`) from 0.3.0 to 0.4.0.
- Since 0.3.0, this brings in the full motion-menu authoring stack
(per-format constraint profiles, semantic menu roles, colour-management
rules, motion menus with animated button highlights, a timeline editor,
and supporting docs — PRs #116–#128) and the audio/DVD-compatibility
fixes (bitrate ceilings, DTS refusal, authored audio track order,
titleset audio consistency validation — PRs #137–#138).

## Test plan
- [x] `pnpm release:version:check` — all manifests synchronised at 0.4.0
- [x] `pnpm validate` (format:check, vitest, tsc build, cargo fmt/clippy
-D warnings, cargo nextest) — all green
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

assets Media import, inspection, and asset management work backend Rust backend, commands, and native runtime behaviour bug Something isn't working build-pipeline Authoring, encoding, packaging, and export pipeline work frontend Frontend UI, styling, and webview behaviour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant