craft-audio: Opus decoder (7/10) (#445)

* craft-audio: Opus decoder

FilmCraft's `filmcraft-opus` decoder as `craft_audio::opus` behind
`opus-decode`: SILK, hybrid and CELT, every frame size, PLC and FEC,
multistream (`Decoder`) and single stream (`StreamDecoder`), 8-48 kHz
output.

- `TrackDecoder` decodes Opus; mono and stereo without an `OpusHead`
  (RTP, MPEG-TS) get a default one. Lost packets use Opus's own
  concealment.
- The native read path takes Ogg Opus (pre-skip and end trimming per
  RFC 7845), and Opus in Matroska/WebM and MP4, where packet starts
  after a seek come from the packets' own lengths.

The RFC 8251 conformance test runs all 36 vectors.
`scripts/fetch-opus-vectors.sh` downloads them (SHA-256 checked) and
the CI test job caches them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 1 (#445)

- CELT concealment keeps its pitch in its own field. It reused postfilter_period_old, which the
  next good frame's post-filter cross-fades from.
- Matroska Opus seeks: a failed packet read no longer caches a wrong start; a one-byte packet
  is read as one byte.
- A malformed OpusHead in Ogg is reported as malformed instead of falling back.
- The libopus oracle tests skip through the testkit, so CRAFT_MEDIA_REQUIRE_ORACLES fails them.
- Decoder::reset documents that it re-arms pre-skip trimming.
- fetch-opus-vectors.sh retries its size probe.
- CI: the vectors are fetched after ffmpeg is installed, and apt's timeout runs as root so a
  stalled apt-get is killed and frees the dpkg lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 2 (#445)

- CELT: the noise-based concealment sets skip_plc, as libopus does, so
  pitch-based concealment waits for two good frames in a row instead of
  extending history that holds noise.
- SILK: decode_only_middle is per call, as in libopus. A lost or mono
  frame no longer reuses the mid-only flag from an earlier frame.
- A damaged Opus packet is concealed at its own length (from its TOC and
  framing) rather than the previous packet's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
This commit is contained in:
authored and GitHub committed 2026-11-11 07:36:57 -05:00
1 parent 5b58973bc6
commit 861c5e172d
34 files changed
+7863 -83

No files matched your search

+23 -10
View File
@@ -3,9 +3,9 @@ name: Shared audio
on:
push:
branches: [main]
paths: ["crates/audio/**", "crates/media/**", "crates/media-testkit/**", "Cargo.toml", "Cargo.lock", ".github/workflows/audio.yml"]
paths: ["crates/audio/**", "crates/media/**", "crates/media-testkit/**", "scripts/fetch-opus-vectors.sh", "Cargo.toml", "Cargo.lock", ".github/workflows/audio.yml"]
pull_request:
paths: ["crates/audio/**", "crates/media/**", "crates/media-testkit/**", "Cargo.toml", "Cargo.lock", ".github/workflows/audio.yml"]
paths: ["crates/audio/**", "crates/media/**", "crates/media-testkit/**", "scripts/fetch-opus-vectors.sh", "Cargo.toml", "Cargo.lock", ".github/workflows/audio.yml"]
workflow_dispatch:
permissions:
@@ -39,7 +39,7 @@ jobs:
# Each feature on its own, so code that needs another feature fails here.
- name: Clippy, each feature alone
run: |
for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" serde; do
for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" opus-decode serde; do
echo "::group::features: ${f:-none}"
cargo clippy -p craft-audio --all-targets --locked --no-default-features ${f:+--features "$f"} -- -D warnings
echo "::endgroup::"
@@ -92,7 +92,7 @@ jobs:
- run: cargo test -p craft-audio --no-default-features --features decode-native --locked
test:
timeout-minutes: 20
name: Audio tests with ffmpeg oracles
name: Audio tests with ffmpeg oracles and the Opus conformance vectors
# Pinned for the ffmpeg its apt ships (8.0), so the files the oracle tests write do not change
# with the runner image.
runs-on: ubuntu-26.04
@@ -105,23 +105,36 @@ jobs:
with:
key: craft-audio-test
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build tests while installing ffmpeg
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/media-fixtures/opus/vectors
# The archive's SHA-256 (scripts/fetch-opus-vectors.sh checks it).
key: opus-testvectors-rfc8251-6b26a22f
- name: Build tests while installing ffmpeg and fetching the Opus vectors
run: |
# The man-db index rebuild after dpkg is slow on hosted runners.
sudo rm -f /var/lib/man-db/auto-update
# A mirror can stall for most of the job's time limit, and the next try usually gets a good one.
install_ffmpeg() {
for attempt in 1 2 3; do
timeout -k 10 180 sudo sh -c 'apt-get update -qq && apt-get install -y -qq --no-install-recommends ffmpeg' && return 0
# timeout runs as root, so a stalled apt-get is killed too and frees the dpkg lock.
sudo timeout -k 10 180 sh -c 'apt-get update -qq && apt-get install -y -qq --no-install-recommends ffmpeg' && return 0
echo "apt: attempt $attempt failed with exit code $?"
timeout -k 10 60 sudo dpkg --configure -a || true
sudo timeout -k 10 60 dpkg --configure -a || true
done
return 1
}
install_ffmpeg > "$RUNNER_TEMP/apt.log" 2>&1 &
apt=$!
# The vectors are fetched after ffmpeg, so the two downloads do not slow each other.
{
install_ffmpeg > "$RUNNER_TEMP/apt.log" 2>&1 || exit 1
scripts/fetch-opus-vectors.sh "$RUNNER_TEMP/media-fixtures/opus/vectors" > "$RUNNER_TEMP/vectors.log" 2>&1 || exit 2
} &
fetch=$!
cargo test -p craft-audio --all-features --locked --no-run
wait "$apt" || { cat "$RUNNER_TEMP/apt.log"; exit 1; }
wait "$fetch" || case $? in
1) cat "$RUNNER_TEMP/apt.log"; exit 1 ;;
*) cat "$RUNNER_TEMP/vectors.log"; exit 1 ;;
esac
ffmpeg -hide_banner -version | sed -n 1p
- run: cargo test -p craft-audio --all-features --locked
env: