Skip to content

feat(pcm)!: align one-shot decode with the sibling io.Reader contract - #55

Merged
tphakala merged 2 commits into
mainfrom
fix/decode-reader-contract-bext-crlf
Sep 5, 2026
Merged

feat(pcm)!: align one-shot decode with the sibling io.Reader contract#55
tphakala merged 2 commits into
mainfrom
fix/decode-reader-contract-bext-crlf

Conversation

@tphakala

@tphakala tphakala commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

Three related changes folded into one PR.

One-shot decode contract (#50). The sibling pcm packages (go-flac, go-aac, go-opus) share DecodeInterleaved(io.Reader) ([]byte, StreamInfo, error) plus DecodeInterleavedLimit, ErrDecodeLimit and DefaultMaxDecodedBytes, so a caller can dispatch decode across codecs on one signature. go-wav diverged: its one-shot took a []byte and returned (StreamInfo, []byte, error). This adds the shared contract and keeps the byte-slice zero-copy fast path under a new name.

  • DecodeInterleaved(r io.Reader, opts ...Option) ([]byte, wav.StreamInfo, error) is new and matches the siblings. It reads the whole stream into memory, bounds it at DefaultMaxDecodedBytes (1 GiB), and returns a wrapped ErrDecodeLimit past that.
  • DecodeInterleavedLimit(r io.Reader, maxBytes int, opts ...Option) is new and takes a caller-chosen ceiling; maxBytes <= 0 is unbounded.
  • DecodeInterleavedBytes(b []byte, opts ...Option) ([]byte, wav.StreamInfo, error) is the former DecodeInterleaved: the zero-copy fast path, kept, with its return order swapped to samples-first so the package is internally consistent.

Breaking change. The exported DecodeInterleaved changes signature and return order. The old byte-slice behaviour lives on under DecodeInterleavedBytes. Every un-updated caller becomes a compile error, because the swapped return types ([]byte vs wav.StreamInfo) cannot be assigned, so the break is loud rather than a silent runtime bug. This lands on a v1.0.0-tagged module, so it needs a release decision: either a /v2 module path (the semver-correct route) or an explicitly documented pre-adoption break, given v1.0.0 is only days old and has no known dependents. No in-repo caller relies on the old surface.

bext free-text CR/LF (#54). Field recorders pack CRLF-separated key=value metadata into the bext Description. The read path returns those bytes verbatim, but the write path rejected them, so a decoded recorder bext could not be re-encoded. Description, Originator and OriginatorReference now accept CR and LF, matching CodingHistory. Every other control byte, and anything outside ASCII, is still refused, so fixed-width field boundaries cannot be corrupted on round-trip.

codecov.yml. Adds a codecov.yml marking project and patch coverage informational, matching the sibling repos, so small diffs with defensive error branches do not fail the advisory patch check.

Related Issues

Closes #50
Closes #54

Test Plan

  • go test -race ./... green; package coverage 97.0%
  • go vet ./..., golangci-lint run ./..., and gofmt clean
  • Fuzz smoke (FuzzDecode, FuzzParseHeader) green
  • New reader-path tests: parity with the byte-slice decode across bit depths, float, companded (A-law) expansion and WithConvertTo; ErrDecodeLimit single-block and multi-block boundaries; unbounded and WithIgnoreLength paths; non-seekable reader
  • New bext tests: CR/LF accepted in all free-text fields, other control bytes and non-ASCII still rejected, full encode/decode/re-encode round-trip of a recorder Description

Summary by CodeRabbit

  • New Features

    • Added reader-based one-shot PCM decoding, including configurable output limits and a 1 GiB default ceiling.
    • Added a zero-copy byte-slice decoding API with documented buffer aliasing behavior.
    • Added support for CR/LF characters in valid bext text fields while continuing to reject other invalid characters.
    • Improved preservation and reporting of invalid bext values during decoding and re-encoding.
  • Documentation

    • Updated API guidance, examples, limits, aliasing behavior, and concurrency guarantees.

Fold three related changes into one PR.

1. One-shot decode contract (issue #50). The sibling pcm packages
   (go-flac, go-aac, go-opus) share DecodeInterleaved(io.Reader) returning
   ([]byte, StreamInfo, error), plus DecodeInterleavedLimit, ErrDecodeLimit
   and DefaultMaxDecodedBytes, so a caller can dispatch decode across
   codecs on one signature. go-wav diverged: its one-shot took a []byte and
   returned (StreamInfo, []byte, error).

   The byte-slice zero-copy fast path is kept, renamed to
   DecodeInterleavedBytes and reordered to samples-first for package
   consistency. New DecodeInterleaved(io.Reader) and
   DecodeInterleavedLimit(io.Reader, maxBytes) are added, bounding the
   in-memory decode at DefaultMaxDecodedBytes (1 GiB) with a wrapped
   ErrDecodeLimit, mirroring go-flac.

   BREAKING CHANGE: the exported DecodeInterleaved signature and return
   order change. The old byte-slice behaviour is available under
   DecodeInterleavedBytes. Every un-updated caller is a compile error (the
   swapped return types cannot be assigned), never a silent bug.

2. bext free-text CR/LF (issue #54). Field recorders pack CRLF-separated
   key=value metadata into the bext Description, which the read path returns
   verbatim but the write path rejected, so a decoded recorder bext could
   not be re-encoded. Description, Originator and OriginatorReference now
   accept CR and LF, matching CodingHistory; every other control byte, and
   anything outside ASCII, is still refused, so fixed-width field boundaries
   cannot be corrupted.

3. Add codecov.yml making project and patch coverage informational,
   matching the sibling repos, so small diffs with defensive error branches
   do not fail the advisory patch check.

Closes #50, #54.
Copilot AI lite review requested due to automatic review settings September 5, 2026 10:12
@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.01887% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pcm/decode_oneshot.go 80.00% 7 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI 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.

🟡 Changes recommended

A couple of newly introduced messages/comments are misleading (decode-limit error op name and bext string-field documentation), and should be corrected before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR aligns go-wav/pcm’s one-shot decode API with sibling codec packages by introducing an io.Reader-based DecodeInterleaved (plus a bounded DecodeInterleavedLimit), while keeping the existing zero-copy byte-slice fast path under DecodeInterleavedBytes. It also relaxes bext free-text validation to allow CR/LF for recorder interoperability and adds a Codecov configuration to make coverage checks informational.

Changes:

  • Add DecodeInterleaved(io.Reader) and DecodeInterleavedLimit(io.Reader, maxBytes) with DefaultMaxDecodedBytes and ErrDecodeLimit.
  • Rename the old byte-slice one-shot decode to DecodeInterleavedBytes([]byte) and align return order to ([]byte, StreamInfo, error).
  • Allow CR/LF in bext free-text fields for field-recorder round-trip, and add Codecov informational coverage config.
File summaries
File Description
README.md Updates docs for new one-shot decode entrypoints and bext CR/LF behavior.
pcm/fuzz_test.go Switches fuzz coverage to DecodeInterleavedBytes and updates messages/ordering.
pcm/example_test.go Updates the existing example and adds a new reader-based decode example.
pcm/encoder_test.go Adjusts tests for the renamed/swap-ordered byte-slice decode helper.
pcm/doc.go Updates package docs to describe the new reader-based contract and the byte-slice fast path.
pcm/decode_oneshot.go Implements DecodeInterleaved, DecodeInterleavedLimit, decode-size limiting, and renames old API to DecodeInterleavedBytes.
pcm/decode_oneshot_test.go Updates one-shot decode tests to target DecodeInterleavedBytes and new return order.
pcm/decode_oneshot_reader_test.go Adds new tests covering reader-based one-shot decode parity and decode limits.
pcm/companded_test.go Updates companded tests for the renamed/swap-ordered byte-slice decode helper.
pcm/bext.go Relaxes bext free-text validation to accept CR/LF and updates comments/validation helpers.
pcm/bext_test.go Extends validation tests for CR/LF acceptance and round-trip behavior.
pcm/bext_decoder_test.go Adds round-trip test for recorder-style CRLF Description through decode/re-encode.
pcm/bench_test.go Renames/updates benchmarks and adds a reader-based benchmark.
codecov.yml Marks project and patch coverage statuses informational.
Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pcm/bext.go Outdated
Comment on lines +50 to +54
// Every string field below is written as ASCII that also admits CR and LF (see
// [Bext.Description]). The three fixed-width fields are NUL-padded to their wire
// width, and Config.validate rejects a value that does not fit rather than
// truncating it; CodingHistory is the exception, the variable-length tail
// appended raw after the fixed body. UMID is binary and written verbatim; the

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in b782f0e. Narrowed the wording to the free-text fields: OriginationDate and OriginationTime are string fields but take the fixed date/time forms checkDateTime enforces, so they do not accept CR or LF.

Comment thread pcm/decode_oneshot.go
Comment on lines +133 to +137
if c.max > 0 && c.n > c.max-len(p) {
return 0, fmt.Errorf(
"go-wav/pcm: DecodeInterleaved: %w: output would exceed %d bytes",
ErrDecodeLimit, c.max)
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Leaving this one as is. The cappedWriter is shared by DecodeInterleaved and DecodeInterleavedLimit (the former delegates to the latter), and it deliberately mirrors the same message the sibling packages go-flac and go-aac emit from their identical shared writer, which is the family consistency this PR is about. The wrapped ErrDecodeLimit sentinel is preserved, so errors.Is matches regardless of entry point. If the shared label is worth changing I'd rather do it across all three packages at once than diverge here.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 48 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 60 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: e5a1ee37-38a5-4508-8057-dba836d95171

📥 Commits

Reviewing files that changed from the base of the PR and between cbadfbd and b782f0e.

📒 Files selected for processing (1)
  • pcm/bext.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 43721da7-5785-4a60-a515-65fbec10affb

📥 Commits

Reviewing files that changed from the base of the PR and between 044b41b and cbadfbd.

📒 Files selected for processing (14)
  • README.md
  • codecov.yml
  • pcm/bench_test.go
  • pcm/bext.go
  • pcm/bext_decoder_test.go
  • pcm/bext_test.go
  • pcm/companded_test.go
  • pcm/decode_oneshot.go
  • pcm/decode_oneshot_reader_test.go
  • pcm/decode_oneshot_test.go
  • pcm/doc.go
  • pcm/encoder_test.go
  • pcm/example_test.go
  • pcm/fuzz_test.go

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


Walkthrough

The PR adds reader-based one-shot PCM decoding with configurable output limits, renames the byte-slice decoder, updates callers, and permits CR/LF in Bext text fields with recorder round-trip coverage.

Changes

PCM decoding and Bext interoperability

Layer / File(s) Summary
Bext text validation and round-tripping
pcm/bext.go, pcm/bext_test.go, pcm/bext_decoder_test.go, README.md
Bext free-text fields accept CR/LF and continue to reject other controls and non-ASCII bytes. Recorder-style metadata now decodes, re-encodes, and decodes again successfully.
Reader-based decoder and byte-slice API
pcm/decode_oneshot.go, pcm/decode_oneshot_reader_test.go, pcm/doc.go, pcm/example_test.go, README.md
DecodeInterleaved accepts io.Reader input with a default 1 GiB limit. DecodeInterleavedLimit adds caller-selected limits. DecodeInterleavedBytes provides the zero-copy byte-slice path.
API migration and validation coverage
pcm/*_test.go, pcm/bench_test.go, pcm/fuzz_test.go, codecov.yml
Benchmarks, examples, fuzz tests, and existing tests use the renamed API and bytes-first return order. Coverage checks are informational.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to cbadf

The reader-based decode API, output limits, byte-slice migration, and Bext CR/LF interoperability changes are ready to merge.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant DecodeInterleaved
  participant Decoder
  participant cappedWriter
  Caller->>DecodeInterleaved: pass io.Reader and optional limit
  DecodeInterleaved->>Decoder: decode stream blocks
  Decoder->>cappedWriter: write decoded samples
  cappedWriter-->>DecodeInterleaved: enforce max decoded bytes
  DecodeInterleaved-->>Caller: return samples, StreamInfo, error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary breaking change: aligning the one-shot PCM decode API with the shared io.Reader contract.
Linked Issues check ✅ Passed The changes satisfy both linked issues. Issue #50 requirements are implemented through reader-based decoding, samples-first returns, decode limits, ErrDecodeLimit, DefaultMaxDecodedBytes, and the dist…
Out of Scope Changes check ✅ Passed The changes are within scope. Documentation, tests, benchmarks, and the Codecov configuration support the stated API, bext interoperability, and coverage objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 46 functions across 12 files. (2 skipped: …
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/decode-reader-contract-bext-crlf

Comment @coderabbitai help to get the list of available commands.

OriginationDate and OriginationTime are string fields but take the fixed
date/time forms checkDateTime enforces, so they do not admit CR and LF;
the previous wording implied every string field does. Addresses a PR
review note.
@tphakala
tphakala merged commit 77d7d8f into main Sep 5, 2026
24 checks passed
@tphakala
tphakala deleted the fix/decode-reader-contract-bext-crlf branch September 5, 2026 10:32
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.

bext round-trip fails for recorder files that put CRLF in Description fix: DecodeInterleaved diverges from the sibling one-shot decode contract

2 participants