Skip to content

feat: add FLAC as a Stream-to-Stream ConvertFile/CutFile destination - #122

Merged
cloud-hai-vo merged 2 commits into
mainfrom
feat/flac-stream-destination
Oct 17, 2026
Merged

cloud-hai-vo merged 2 commits into
mainfrom
feat/flac-stream-destination

Conversation

@cloud-hai-vo

Copy link
Copy Markdown
Contributor

Summary

  • Continues docs/feature-roadmap.md item 1's own "destination-Stream" half, which the owner's own cron instructions called out by name as an example next step. WAV/AIFF/AU were already done (none of the three need Stream.CanSeek); FLAC is the first destination here that genuinely does.
  • FlacEncoder.OpenSession(Stream destStream, ...) -- a new overload, mirroring WavWriter/AiffWriter/AuWriter's own Create(Stream, ...) overloads. Requires destStream.CanSeek (throws ArgumentException up front if not) because FlacEncoderSession.Finish() still seeks back to a fixed byte offset to patch STREAMINFO's provisional totalSamples/MD5/block-size-range fields once their real values are known -- exactly the way the path-based overload's own FileStream destination already does. Does not dispose the caller's stream.
  • FlacEncoderSession's own destination field was widened from FileStream to Stream, plus a leaveOpen flag (mirroring WavWriter's own _leaveOpen convention) so Dispose() only closes a stream this type actually owns.
  • Channel/bits-per-sample/tag validation was factored out of the existing path-based OpenSession into a shared private helper (ValidateAndGetCommentListBytes) so the two overloads can never drift apart on what's a valid request.
  • Wired into AudioCutter.Stream.cs's OpenSinkStream as the AudioFormat.Flac case -- unlike WAV/AIFF/AU, no DeferredFixedHeaderSink wrapper is needed at all, since FLAC's own header-patch-after-the-fact design never needed an exact frame count up front in the first place (matching the existing path-based AudioCutter.cs dispatch for .flac, which already calls FlacEncoder.OpenSession directly with no such wrapper).
  • Found and fixed two pre-existing, now-stale tests during this change: NativeEncoderTest.ConvertFile_Stream_WithUnsupportedDestFormat_Should_Throw and AudioCutterStreamTest.Convert_Stream_WithUnsupportedDestFormat_Should_Throw both used AudioFormat.Flac as their "still unsupported" example destination -- now true, so both were switched to AudioFormat.Wma (still genuinely unsupported as a destination).
  • Found and fixed stale documentation unrelated to this change but touched by it: CLAUDE.md's own AudioCutter.Stream.cs row still said "Destination: WAV only so far", which had already drifted out of sync with reality once AIFF/AU landed in an earlier PR; corrected while updating the same line for FLAC.
  • Docs synced: CLAUDE.md, README.md, llms.txt, llms-full.txt, docs/feature-roadmap.md item 1's own status note.

Test plan

  • New FlacEncoderTest cases: null stream, non-seekable stream (throws with "seekable" in the message), zero channels, bits-per-sample out of range (0/33), invalid tag field name (throws before a single byte reaches the caller's stream), exact round-trip via a MemoryStream destination, and Dispose() not disposing the caller's stream.
  • New AudioCutterStreamTest cases: WAV source → FLAC Stream destination round-trip (exact samples), non-seekable FLAC destination throws through the full AudioCutter.Convert call, and a ResamplingTransform pipeline into a FLAC Stream destination writes the correct total frame count.
  • New shared test utility NonSeekableWriteStream (the write-side counterpart to the existing NonSeekableStream).
  • Full suite (dotnet test, net10.0 locally): 1729 passed (1718 baseline + 11 new), 1 pre-existing unrelated skip, 0 failed.
  • Full solution build: 0 warnings, 0 errors.

🤖 Generated with Claude Code

cloud-hai-vo and others added 2 commits October 17, 2026 19:04
FlacEncoder.OpenSession gained a Stream-based overload, mirroring WavWriter/
AiffWriter/AuWriter's own Create(Stream, ...) overloads but genuinely
requiring CanSeek (unlike those three): FlacEncoderSession.Finish() seeks
back to patch STREAMINFO's provisional totalSamples/MD5/block-size-range
fields once the real values are known, so a non-seekable destination throws
ArgumentException up front instead of failing confusingly mid-Finish().

FlacEncoderSession's own destination field was widened from FileStream to
Stream plus a leaveOpen flag (mirroring WavWriter's own convention) so
Dispose() only closes a stream this type actually owns. Channel/bits-per-
sample/tag validation was factored into a shared private helper so the
path-based and Stream-based OpenSession overloads can't drift apart.

Wired into AudioCutter.Stream.cs's OpenSinkStream as the AudioFormat.Flac
destination case -- no DeferredFixedHeaderSink needed, since FlacEncoder
already patches its header after the fact rather than needing an exact
frame count up front. Fixed two pre-existing tests that used
AudioFormat.Flac as their "still unsupported" example destination format,
now stale.

Also fixed a stale doc comment/CLAUDE.md note that had drifted out of sync
with the codebase (still said "WAV only" for the Stream destination set,
when AIFF/AU had already landed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ion mid-stream after Finish()

Self-review finding: unlike WavWriter/AiffWriter/AuWriter's own Stream-based
destinations (which never seek back), FlacEncoderSession.Finish()'s
STREAMINFO patch-seek leaves the caller's stream Position shortly after the
start of the stream, not at the true end. Length is unaffected and still
correct. Documented on the new overload so a caller doesn't get surprised
relying on Position to mean "how much was written".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cloud-hai-vo
cloud-hai-vo merged commit 18f151a into main Oct 17, 2026
9 checks passed
@cloud-hai-vo
cloud-hai-vo deleted the feat/flac-stream-destination branch October 17, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant