Commit Graph
22 Commits
Author SHA1 Message Date
Dylan M. Taylor 119f6e252e craft-audio: integer PCM of WAV and AIFF files bit for bit, and minimal files around it (#917)
wav_pcm and aiff_pcm (feature decode-native) return a file's integer PCM as stored
(RawPcm: little-endian interleaved bytes, 8-bit unsigned, with channels, rate and
bits), built on the existing WAV and AIFF chunk parsers. pcm_wav_file,
pcm_wav_header, pcm_aiff_file and pcm_aiff_header (feature pcm-encode) write the
44-byte canonical WAVE header and the plain AIFF header around such bytes.

For essence embedded in AAF and OMF files, which is moved bit for bit and whose
descriptors hold a bare header as a summary. The writers are FilmCraft's
(filmcraft-interchange wav.rs); the readers replace its second WAV/AIFF parser
with this crate's.
2026-11-11 18:52:13 -05:00
Dylan M. Taylor 56d088bf04 craft-audio: flac::Encoder, the FLAC packet encoder, is public (#809)
* craft-audio: FilmCraft's FLAC encoder and MP1/MP2 decoder speed-ups

FLAC: per-order predictor kernels, merged Rice partition sums, the LPC
window kept per block length, and batches consumed by offset
(filmcraft 5eaff794, d9dad4a5). The output is unchanged: 64 golden cases
hash the same before and after.

MP1/MP2: the faster synthesis filterbank (filmcraft 51c755e5), with its
bit-exact test.

* craft-audio: FilmCraft's AAC and Opus decoder speed-ups and refactors

AAC: table-driven Huffman decoding, per-stage FFT twiddles and fused
stages, reused per-channel work buffers, and the encoder's memoised
per-band work (filmcraft f9627280, d587ceed, 6121635c, dc124ab7,
7a532eca).

Opus: radix-2/3/4 FFT kernels in the CELT IMDCT, the PVQ table built
once, one de-emphasis loop for both channels, the SILK resampler
without per-sample divisions and with eight partial sums, concealment
and FEC capped at 120 ms (filmcraft bf7ac420, 12e32026, c4dc64d1,
eb352ec8, 87b8e22d, 014b5e56, 78d77b72).

The per-frame mid-only flag this crate fixed in the SILK decoder is
kept in the refactored decode.

* craft-media: Ogg CRC-32 by slice-by-16

FilmCraft's 6763f9bf: sixteen independent lookups per 16 input bytes, so
opening a file is several times faster. Checked against a bit-at-a-time
reference on every short length and alignment, and across every split.

* craft-audio: SoundCraft's CAF cookie-size and FLAC vendor-length checks

A CAF magic cookie size above the signed range is refused instead of
passed to symphonia (soundcraft 3bd3965), and a Vorbis comment block
whose vendor string claims more than the block holds is refused
(soundcraft ddfe991). Each with its regression test.

* craft-audio: AC-3 reads bits through the shared BitReader

FilmCraft's 1c9c2e82: the AC-3 decoder's private bit-at-a-time reader is
replaced by craft-media's BitReader, with its two tests: the decoded
output of 2,000 synthetic and 12 real syncframes (and their damaged
variants) hashes the same as before, and the reader agrees with the old
one on random reads, skips and overruns. The three AC-3 features now
turn on craft-media's bitstream module.

* craft-audio: the quantiser rounds without a libm call

SoundCraft's d293336, the audio-io part: round-half-away by integer
arithmetic in the bounce's inner loop, with its test against f64::round
over the quantiser's range.

* craft-audio: flac::Encoder, the FLAC packet encoder, is public

FilmCraft muxes FLAC into its own outputs from integer samples, so it
needs the encoder below FileEncoder: planar samples in, frames out, and
the STREAMINFO to patch in at the end. The module is behind flac-encode,
its errors convert to AudioError::Encode, and pending_samples says how
long the final frame will be.
2026-11-11 11:48:37 -05:00
Dylan M. Taylor 44b4a23137 craft-audio, craft-media: the changes FilmCraft and SoundCraft made to their copies since extraction (#807)
* craft-audio: FilmCraft's FLAC encoder and MP1/MP2 decoder speed-ups

FLAC: per-order predictor kernels, merged Rice partition sums, the LPC
window kept per block length, and batches consumed by offset
(filmcraft 5eaff794, d9dad4a5). The output is unchanged: 64 golden cases
hash the same before and after.

MP1/MP2: the faster synthesis filterbank (filmcraft 51c755e5), with its
bit-exact test.

* craft-audio: FilmCraft's AAC and Opus decoder speed-ups and refactors

AAC: table-driven Huffman decoding, per-stage FFT twiddles and fused
stages, reused per-channel work buffers, and the encoder's memoised
per-band work (filmcraft f9627280, d587ceed, 6121635c, dc124ab7,
7a532eca).

Opus: radix-2/3/4 FFT kernels in the CELT IMDCT, the PVQ table built
once, one de-emphasis loop for both channels, the SILK resampler
without per-sample divisions and with eight partial sums, concealment
and FEC capped at 120 ms (filmcraft bf7ac420, 12e32026, c4dc64d1,
eb352ec8, 87b8e22d, 014b5e56, 78d77b72).

The per-frame mid-only flag this crate fixed in the SILK decoder is
kept in the refactored decode.

* craft-media: Ogg CRC-32 by slice-by-16

FilmCraft's 6763f9bf: sixteen independent lookups per 16 input bytes, so
opening a file is several times faster. Checked against a bit-at-a-time
reference on every short length and alignment, and across every split.

* craft-audio: SoundCraft's CAF cookie-size and FLAC vendor-length checks

A CAF magic cookie size above the signed range is refused instead of
passed to symphonia (soundcraft 3bd3965), and a Vorbis comment block
whose vendor string claims more than the block holds is refused
(soundcraft ddfe991). Each with its regression test.

* craft-audio: AC-3 reads bits through the shared BitReader

FilmCraft's 1c9c2e82: the AC-3 decoder's private bit-at-a-time reader is
replaced by craft-media's BitReader, with its two tests: the decoded
output of 2,000 synthetic and 12 real syncframes (and their damaged
variants) hashes the same as before, and the reader agrees with the old
one on random reads, skips and overruns. The three AC-3 features now
turn on craft-media's bitstream module.

* craft-audio: the quantiser rounds without a libm call

SoundCraft's d293336, the audio-io part: round-half-away by integer
arithmetic in the bounce's inner loop, with its test against f64::round
over the quantiser's range.
2026-11-11 11:32:33 -05:00
alehelandClaude Opus 5.5 542ca365e2 craft-audio: review fixes, round 7 (#443)
Reading:
- an element of another type or a tag a program config does not list is
  refused, not written to another channel; only an LFE coded as an SCE
  is still allowed, as ffmpeg allows it
- an explicit sampling rate of 0 is refused
- a packet's later ADTS frames are searched for SBR
- a multi-frame ADTS packet that fails leaves the decoder as it was, and
  one whose frames change the channel count is an error
- the whole ADTS frames before a damaged end of a packet decode
- a failed read of a raw stream's first frames is an I/O error
- an MP4 sample of several ADTS frames past the lookahead keeps its length
- an Ogg file with leading bytes keeps its track order and mask comments

Writing:
- ProgramConfig::write, AudioSpecificConfig::to_bytes and
  AdtsHeader::write refuse what their fields cannot hold

Tests:
- the routing test checks one alignment for the ffmpeg version under test
- a default decode past an Ogg stream symphonia has no track for

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 06:37:50 +01:00
alehelandClaude Opus 5.5 04df866591 craft-audio: review fixes, round 17 (#442)
The Ogg track tests set their files' serial numbers instead of writing
the files until ffmpeg happens to pick the wanted order. They rewrite
each page's serial and checksum, so both orders run with one ffmpeg call
and do not depend on ffmpeg's serials being random.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:06:42 +01:00
alehelandClaude Opus 5.5 e8b7c40ea0 craft-audio: review fixes, round 16 (#442)
The Ogg track tests no longer depend on chance. Each writes its file
until the serial numbers are in the order it asks for, in both orders,
so the reversed order that told the old code apart always runs. A file
this ffmpeg cannot write goes through `skip`, which fails the test when
oracles are required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 19:45:11 +01:00
alehelandClaude Opus 5.5 a4f02a3854 craft-audio: review fixes, round 15 (#442)
A track index in an Ogg file means the same stream everywhere: the
file's order, as `tracks` lists them. Streams the native path does not
read fell back to symphonia with the same index, but symphonia orders
tracks by serial, which ffmpeg picks at random. A file could decode the
wrong track, or fail on a FLAC stream listed before an Opus one.

The tests write each file eight times, so both serial orders occur, and
cover a track with no mask comment next to one with a mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:32:10 +01:00
alehelandClaude Opus 5.5 2f4fb8bfe0 craft-audio: review fixes, round 14 (#442)
Each FLAC stream in an Ogg file reads its own channel mask comment. The
tags symphonia's reader collects do not say which track they are from,
so a track could get another track's mask. The masks now come from
craft-media's Ogg demuxer, by serial, which is symphonia's track ID.

The Ogg layout test also covers 7 channels with no comment and a zero
mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:09:27 +01:00
alehelandClaude Opus 5.5 99dcf517a4 craft-audio: review fixes, round 13 (#442)
FLAC in Ogg reads its layout the way a FLAC file does: from the mask
comment, or else the one decoders assume for the count. Without this, a
6-channel stream with no comment reported back surrounds in Ogg and side
surrounds in a FLAC file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 14:49:49 +01:00
alehelandClaude Opus 5.5 57f5d063fe craft-audio: review fixes, round 12 (#442)
- FLAC files read their layout from the channel mask comment, or else
  the one decoders assume for the count, which is what the writer omits
  the comment for
- an unbuilt format is Unsupported before the layout is checked
- FLAC encode no longer reserves half the PCM size up front
- flac-encode's lib tests stream a file and compare it with encode
- the seek test compares samples after the seek

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:51:18 +01:00
alehelandClaude Opus 5.5 1a9f579a07 craft-audio: review fixes, round 11 (#442)
No change in output: the 74 oracle test streams are byte-identical.

- The Wasted case is handled in one function, without the unreachable arm
  and the wrapper round 8 added.
- The bitstream writer uses predict::fold, the tested copy, not its own.
- STREAMINFO's MD5 may be 0 (RFC 9639 §8.2), so the comment no longer says
  the RFC requires it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 afdc045e94 craft-audio: review fixes, round 10 (#442)
Tests only; each was confirmed to fail on a mutated encoder.

- Every sample rate code in the frame header table.
- Signals that reach the Rice parameter and LPC shift limits, where a
  value past the limit gives a file decoders refuse.
- ENCODE_EXTENSIONS lists flac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 5617261fdc craft-audio: review fixes, round 9 (#442)
Refuse a linear predictor whose prediction leaves the i32 range. Such a
stream is valid, but symphonia adds the prediction to the residual as an
i32, which panics in a debug build. The round 8 residual limit made this
reachable at 24 bits; without the override, 13 of 240 clipped 32-bit test
streams failed in a debug build and none do now, for 0.09% more bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 82f63f3a4f craft-audio: review fixes, round 8 (#442)
- Code wasted bits (RFC 9639 §9.2.2): low bits that are zero in every
  sample of a subframe were stored anyway, so 16-bit audio exported at 24
  or 32 bits grew by the padding.
- Allow residuals up to the 32-bit limit (§9.2.7.3). The limit was half
  that, so at 32 bits a transient forced its whole block to verbatim.
- README: 32-bit FLAC codes stereo channels independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 c008e8ee07 craft-audio: review fixes, round 7 (#442)
- Test the high bits of STREAMINFO's total sample count, and that a total
  past 36 bits is left unknown.
- README limits: the usual 5- and 6-channel layouts depend on the mask
  comment too; say what is outside the streamable subset.
- Comments: 32-bit code 0 does not decode everywhere and leaves the
  subset; the oracle test helper is in craft_media_testkit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 430ad15683 craft-audio: review fixes, round 6 (#442)
Tests only, plus docs; the encoder does not change. Each new check was
confirmed to fail on a mutated encoder.

- Tests: frame numbers count up, LPC and Rice partitions and stereo
  decorrelation save the space they should, FileEncoder records a FLAC
  layout, the smallest frame anywhere reaches STREAMINFO, Writer::write
  applies the frame limit, finish leaves the sink at the end, the exact
  655350 Hz cap, the sample size codes.
- README limits: a FLAC layout decoders would not assume depends on its
  mask comment, and ffmpeg and flac ignore a zero mask.
- The encode example's comment named no referent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 82424e59c3 craft-audio: review fixes, round 5 (#442)
- libFLAC and ffmpeg read a FLAC with 5 or 6 channels and no mask comment
  as having side surrounds, so the comment is now written for anything
  else, including the crate's usual back-surround layouts. The round 3
  check treated back surrounds as FLAC's assignment, which lost them.
- Tests: the mask comment rules, opposite-polarity stereo, STREAMINFO
  block and frame sizes.
- A stale comment said only WAV records the layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 1648f33e6a craft-audio: review fixes, round 3 (#442)
FLAC files record the speaker layout when FLAC's channel assignment
does not already say it, as a WAVEFORMATEXTENSIBLE_CHANNEL_MASK comment.
FLAC's 7-channel order is 6.1 with side surrounds, so a buffer in the
crate's usual 7-channel layout (backs and a back centre) was decoded by
ffmpeg and libFLAC with three channels on the wrong speakers; explicit
layouts such as 5.1 with side surrounds were dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 76df23e86e craft-audio: review fixes, round 2 (#442)
- check_room docs: FLAC holds at most 2^36 - 1 frames.
- The FLAC round trip covers every channel count from 1 to 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 7c88c201a5 craft-audio: review fixes, round 1 (#442)
- FileEncoder/encode for FLAC hand the encoder one 4096-frame block at a
  time: a whole-buffer encode drained the pending samples quadratically
  (480 s of stereo took 19 s, now 5 s) and copied the whole input twice.
- Frame headers name sample rates outside the table (codes 12-14), so
  streams at rates like 11025 or 37800 Hz stay in the streamable subset.
- The decode doctest needs symphonia to read FLAC at this point.
- README: FLAC output is verified against ffmpeg, not the flac tool.
- Tests: keep the AIFF force-RF64 check; exact 2^36 frame boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
alehelandClaude Opus 5.5 550732201c craft-audio: FLAC encoder
FilmCraft's FLAC encoder behind `flac-encode`, kept over SoundCraft's:
linear prediction, stereo decorrelation and the STREAMINFO MD5 give
0.1-37% smaller files on 12 test signals, and the output is bit-exact
through `flac` and ffmpeg. It is 3-5x slower than SoundCraft's, still
about 56x real time.

`lossless-encode` is now `pcm-encode` plus `flac-encode`, and
`EncodeFormat::Flac` writes it through `encode` and `FileEncoder`.
With only one of the two features, the other's formats are refused as
`Unsupported`.

The tests that use FLAC files as decode fixtures (truncation,
corruption, metadata bounds, zero-frame files, container preflight)
come with it, as do the FLAC oracle tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:35:44 +01:00
Aleksander HelgakerandClaude Opus 5.5 38740b43a9 craft-audio: core types, WAV/AIFF read and write, peaks, symphonia (#441)
* craft-audio: core types, WAV/AIFF read and write, peaks, symphonia

The new crate without any codec module: what every later codec plugs
into.

- Shared types: `AudioBuffer`, `AudioInfo`, `ChannelLayout`/`Speaker`,
  `AudioError`, `EncodeOptions` (built with `new` and `with_*`),
  `EncodeFormat`, `BitDepth`, `BwfInfo`, `SampleFormat`.
- WAV/BWF/RF64 and AIFF/AIFF-C reading and writing from SoundCraft's
  `soundcraft-audio-io`, kept over EffectCraft's and FilmCraft's
  smaller writers (only it has BWF, dither, any speaker mask and
  streaming without a known length). `encode` and `FileEncoder` behind
  `pcm-encode`; `lossless-encode` and `encode` turn it on.
- `decode`, `probe`, `tracks` and the streaming, seekable `Decoder`
  (feature `decode-native`). The native path reads WAV, AIFF, and PCM in
  MP4/MOV and Matroska through craft-media's demuxers, packet by packet,
  with edit lists and `CodecDelay`/`DiscardPadding` applied.
  `TrackDecoder` decodes PCM; each codec PR adds its codec to it.
- symphonia 0.6 (MPL-2.0, used unmodified) behind the `symphonia`
  feature, part of `decode`: the fallback for every format with no
  native decoder yet. Its size fields are checked before it reads, and
  its panics are caught where they unwind.
- `peaks`: the min/max/RMS pyramid for waveform drawing.

Workspace: craft-media is a workspace dependency without default
features, so an encode-only craft-audio does not build the MP4
demuxer, and `symphonia-bundle-flac` builds without overflow checks in
dev profiles (its 32-bit LPC restore wraps on valid input).

CI: `audio.yml`, with clippy per feature, rustdoc, WASM, MSRV, and the
tests with ffmpeg oracles.

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

* craft-audio: review fixes (CodeRabbit on #441)

- Reading: AIFF that declares frames without an SSND chunk is Malformed; a Matroska PCM track's
  length counts from its first frame; seek targets saturate; an empty FLAC has one track; a
  failed header read is an Io error; deinterleave allocates fallibly; concealed silence is capped
  like a decoded packet, and symphonia's uses the damaged packet's own duration when known.
- Peaks sum squares in f64, so loud float samples give a finite RMS.
- Writing: .aifc maps to AIFF; BWF text is cut at a character boundary; a WAV byte rate past
  32 bits is an error; the FileEncoder docs say nothing may follow the sink's position.
- README: the Limits list says which codecs go through symphonia and which MP4 edit lists apply.
- Tests for each, alias and dither coverage, and a FLAC case in the routing oracle.

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

* craft-audio: review fixes, second round (CodeRabbit on #441)

- Every header read is checked: `Native::open`, `native::tracks` and the no-symphonia error read
  it with `head_checked`. craft-media demuxer errors that are I/O failures (not a file ending
  early) are returned as `AudioError::Io` instead of sending the file to the other path.
- A Matroska track's presented length runs from its first frame plus `CodecDelay` (unclamped,
  in timestamp ticks with the delay rounded to one) to its end.
- symphonia's concealment allocates fallibly, and a damaged first packet is concealed when the
  demuxer gives its duration. `deinterleave` reserves its channel list fallibly too.
- `TrackDecoder::conceal` documents its cap at the decode limit.
- Tests: an I/O failure past the first 64 KiB, codec delay with the first block at -20, 0 and
  50 ms (checked with `probe`), a seek back after an overshoot, and the loud-sample RMS value.

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

* craft-audio: TrackDecoder::conceal returns a Result

Concealment allocates fallibly and fails with TooLarge when the silence would pass the 2^30-sample
decode limit, instead of returning less than the packet's length. The native path, the README,
the packets example and the doc example pass the error on.

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

* craft-audio: review fixes, third round (CodeRabbit on #441)

- Concealed silence is held to the decode limit inside pcm::silence, so symphonia's path checks
  frames × channels too, and symphonia's decoded audio is copied into fallibly allocated planes.
- TrackDecoder::reset forgets the last packet's length.
- Without symphonia, a damaged MP4, Matroska or Ogg file (truncated, invalid, no moov) is
  Malformed instead of Unsupported; a feature the demuxer does not read still names the build.
- Tests: an MP4 with no moov in a native-only build, conceal after a reset and past the limit.

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

* craft-audio: review fixes, fourth round (CodeRabbit on #441)

- A file the native demuxers find damaged is Malformed with symphonia too, when symphonia calls
  it unsupported; before, an ftyp-only MP4 was Unsupported in symphonia builds.
- symphonia's decoded blocks are held to the 2^30-sample decode limit before allocating.
- Symph::seek forgets the last packet's length, so a lost packet after a seek is not concealed
  with the length of one before it.

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

* craft-audio: review fixes, fifth round (CodeRabbit on #441)

- The damaged-file test covers Matroska as well as MP4, and fails when either branch of
  native::damage is removed.
- native::damage leaves out Ogg: ogg::open reports no damage (it skips damaged pages and calls a
  file with none it can read not Ogg), and symphonia reports damaged Ogg as malformed itself.

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

* craft-audio: run the ffmpeg oracle tests on ubuntu-26.04

ubuntu-latest's apt ships ffmpeg 6.1.1, which writes MP2 and E-AC-3 in
Matroska with ends a few milliseconds early, so the routing tests on the
later codec PRs cannot pass on it. ubuntu-26.04 ships ffmpeg 8.0.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:25:57 +01:00