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>
- 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>
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>
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>
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>
- 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>
- 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>
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>
- 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>
The PICTURE metadata test inserts its block without the last-block flag:
the encoder's VORBIS_COMMENT follows it, and the flagged version was a
broken metadata chain that only decoded by resyncing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
- 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>
- 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>
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>
* 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>
Nav kept the rows of the last frame that drew them. When a title click
closed the popup before its rows drew again, Enter or Space ran the
cached command with its old enabled state. Nav::load now clears the rows,
so the keys act only on rows drawn this frame under the current gate.
- docking: apply_with_permissions refuses a move or open into a floating
window for a panel that is not floatable (Move, OpenAt, MovePanel and
group/column/window moves), and a Group placement at a floating anchor,
which becomes a split of its frame, needs accepts_splits.
- fonts: the served-font loader keys fetched files, in-flight requests,
failures and pending UI faces by (url, integrity), and installs bytes
only into faces requiring the integrity they were fetched under
(Stack::provide_fetched); the public Stack::provide is unchanged.
* text: keep hyphenation and line-breaking work linear and bounded
- hyphen::dict: inflate with a 64 MiB limit (MAX_INFLATED).
- hyphenate_word: walk the sorted break offsets with one iterator.
- patgen::score: index expected breaks once per word.
- patterns: FxHasher's finish rotates the multiplied bits down so packed
keys sharing low bits spread over buckets; at most MAX_PATTERNS (1M).
- linebreak: Sums precomputes each item's next box or forced break,
so next_start is a lookup instead of a scan per candidate.
* address review: refuse to serialize hyphenation dictionaries past the inflate limit
* print: charge skipped whitespace and comments to the lexer's token budget
Lexer::skip_ws scanned whitespace and comments without charging them,
so an annotation object behind a long comment was rescanned in full for
every reference to it, outside the per-object and job-wide budgets. The
skipped bytes now count like tokens, and the scan stops at the budget.
* address review: fail objstm load when header lexer exhausts, restore token charge on failed reference lookahead
* address review: charge objstm header lexer tokens to the job budget
One region_index/without_index helper in lang.rs serves region(), pack_lang::names and packs::locales_for; tests pin the sr-RS-ijekavsk names and the length cut of sr-Cyrl-RS-ijekavsk.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
Review of the elision fix: the cheap cost went to every apostrophe form the
dictionary accepts, so English possessives (churchs → church's), contractions
(neer → ne'er) and Catalan enclitics (acusit → acusi't) jumped ahead of the
one-letter fix. A missing apostrophe now ranks first only before a dictionary
word of two letters or more (j'adore, l'home, qu'il, dell'anno) and only when
the language elides (new Tuning::elision, off for English). Any other form costs
COST_MISTYPE + 1, after every one-letter fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
Suggestions tried a missing space but never a missing apostrophe, so French
"Jadore" got "Adore, J adore, Jade" and never "J'adore". Round 4 now also
offers a'b after a short first part (at most 7 letters), ranked before every
split. The dictionary's own check decides whether the elision is right, so
French, Italian and Catalan elision (l'acqua, dell'anno) work and a dictionary
without elided forms is unaffected. Suggestions keep the writer's case and the
apostrophe the dictionary's OCONV writes (J’adore). Costs at most one lookup
per split.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
A track could list any number of ContentEncoding entries, and readers walk
the whole list for every frame they return (frames_readable,
stripped_header). A track with more than 16 entries, far beyond the one or
two a muxer writes, is now treated as having an unsupported compression, so
its frames are returned as stored and the list is never long.