fix(engine): honor decoded sample count in Wave/MF ReadAudio at EOF#1997
fix(engine): honor decoded sample count in Wave/MF ReadAudio at EOF#1997yuto-trd wants to merge 2 commits into
Conversation
The NAudio-backed readers (WaveReader, MFReader) discarded the float count returned by ISampleProvider.Read and always returned a full requested-length Pcm with `return true`. Because Read never returns a negative value, the `count >= 0` guard's false branch was effectively dead, so a read crossing end-of-stream reported success with a misleading full-length buffer, and a read starting past EOF still succeeded. Honor the actual decoded count instead: the provider's element count maps to frames via /2 (ToStereo gives 2 floats per frame), so allocate the Pcm at the decoded frame count and return false only when nothing was decoded. This matches the canonical FFmpegReaderProxy, which already builds its Pcm from the worker's reported sample count. Behavior-preserving for the only consumer: SourceNode.Process already copies with Math.Min(pcm.NumSamples, buffer.SampleCount), so a shorter Pcm is absorbed as trailing silence rather than over-read. No public signature changes. Also document the contract on MediaReader.ReadAudio and clarify that AudioStreamInfo.NumChannels (source metadata) intentionally differs from Pcm.NumChannels (always stereo). AVFReader's native zero-fill path already satisfies the documented contract; reporting an actual short read there needs an extended C ABI + dylib rebuild and is left as a follow-up. Refs: Project #9 "AI Review" item "Engine: review the audio read contract (fixed-length/EOF, channel count, A/V sync)" Claude-Session: https://claude.ai/code/session_01UrSHLdLaod5Uih8VuoK4eJ
|
Warning Review limit reached
More reviews will be available in 9 minutes and 13 seconds. Learn how PR review limits work. To continue reviewing without waiting, enable usage-based billing in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughDocuments the ChangesAudio Short-Read Contract
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
There was a problem hiding this comment.
Pull request overview
This PR aligns the MediaReader.ReadAudio behavior across backends by ensuring NAudio-based readers (WaveReader, MFReader) honor the actual decoded sample count near EOF instead of always returning a full requested-length Pcm. It also documents the intended contract and adds unit coverage for EOF edge cases.
Changes:
- Update
WaveReaderandMFReaderto size the returnedPcmfrom the provider’s decoded element count (frames = floats/2) and returnfalseonly when no frames were produced. - Document
MediaReader.ReadAudioand clarify thatAudioStreamInfo.NumChannelsis source metadata and may differ from the decoded stereo output. - Add new
WaveReaderunit tests covering in-range reads, reads crossing EOF, and reads starting past EOF; annotate AVFoundation’s current zero-fill behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Beutl.UnitTests/Engine/Media/Wave/WaveReaderTests.cs | Adds unit tests validating correct ReadAudio behavior at/near EOF for WaveReader. |
| src/Beutl.Engine/Media/Wave/WaveReader.cs | Fixes WaveReader.ReadAudio to respect actual decoded frame count. |
| src/Beutl.Extensions.MediaFoundation/Decoding/MFReader.cs | Applies the same decoded-frame-count fix to the Media Foundation reader. |
| src/Beutl.Extensions.AVFoundation/Decoding/AVFReader.cs | Documents AVF’s current zero-fill-at-EOF behavior relative to the contract. |
| src/Beutl.Engine/Media/Decoding/MediaReader.cs | Adds XML docs defining the ReadAudio contract for callers/backends. |
| src/Beutl.Engine/Media/Decoding/AudioStreamInfo.cs | Documents that NumChannels reflects source metadata, not the decoded stereo output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address Copilot review on PR #1997. The EOF fix made Wave/MF return false for length == 0 (frames == 0), but the canonical FFmpeg/AVF backends — and the previous Wave/MF behavior — treat a zero-length request as a successful no-op returning an empty stereo Pcm. SoundSource can pass length == 0 via TimeSpan conversions, so special-case it before the short-read check to keep the contract consistent across backends. Also reword the ReadAudio docs: NumSamples is "the number present in the returned buffer" (not "actually decoded", which was inconsistent with the allowed zero-filled-tail shape), and the return contract now documents that length == 0 yields an empty buffer with true. Claude-Session: https://claude.ai/code/session_01UrSHLdLaod5Uih8VuoK4eJ
|
No TODO comments were found. |
Minimum allowed line rate is |
Description
Unifies the
MediaReader.ReadAudiocontract across backends. The NAudio-backed readers (WaveReader,MFReader) discarded thefloatelement count returned byISampleProvider.Readand always returned a full requested-lengthPcmwithreturn true. BecauseReadnever returns a negative value, thecount >= 0guard's false branch was effectively dead — so:This PR makes both readers honor the actual decoded count: the provider's element count maps to frames via
/2(ToStereo()gives 2 floats per frame), so thePcmis allocated at the decoded frame count andfalseis returned only when nothing was decoded. This matches the canonicalFFmpegReaderProxy, which already builds itsPcmfrom the worker's reported sample count.It also documents the contract:
MediaReader.ReadAudio—NumSamplesis the actually-decoded count (may be< lengthnear EOF), output is always stereo,falsemeans nothing could be read, and callers must copy withMath.Min.AudioStreamInfo.NumChannels— source metadata (display/thumbnails), intentionally differing fromPcm.NumChannels(always 2). No behavior change here per the chosen scope.AVFReader's native path already zero-fills uncovered ranges (trailing silence), which satisfies the documented contract; reporting an actual short read there needs an extended C ABI +.dylibrebuild and is left as a follow-up (commented in code).Affected areas
Beutl.Engine(rendering / scene / track)Beutl.ProjectSystem(project / document persistence)Beutl.Editor,Beutl.Editor.Components,Beutl.Controls)Beutl.Extensibility(plugin abstractions)Beutl.NodeGraph(node editor)Beutl.FFmpegIpc/Beutl.FFmpegWorker(media IPC boundary)Beutl.Api(server API client)Beutl.Extensions.MediaFoundationandBeutl.Extensions.AVFoundationBreaking changes
None. The canonical
FFmpegReaderProxyalready behaves this way, the only first-party consumer (SourceNode.Process) already copies withMath.Min(pcm.NumSamples, buffer.SampleCount)so a shorterPcmis absorbed as trailing silence, and the change only shrinks an over-reported count to the truth. No public signature changes. Third-partyMediaReadersubclasses that return full-length zero-filled buffers remain valid under the documented<remarks>clause.Test plan
tests/Beutl.UnitTests/Engine/Media/Wave/WaveReaderTests.cs(new) — cross-platform, drives a real WAV fixture written viaNAudio.Wave.WaveFileWriter:NumSamples == requested, samples match the written rampNumSamples == actual(< requested)false,sound == nullEngine.Audiosuite (200 tests) green;SourceNode/Composerunaffected.MFReader: build-verified only (PlatformTarget=x64, not loadable on arm64 macOS); the fix is byte-for-byte symmetric with the unit-testedWaveReaderchange.Fixed issues / References
Follow-ups
beutl_avf_reader_read_audioC ABI with an out-sample-count, update the SwiftreadSamples+ P/Invoke, rebuild the.dylib, add a macOS-gated test. Separate PR.MFDecoder._firstGapTimeStamp, video path only), out of scope for this contract task; leave to the existing MF follow-up item.FFmpegReaderProxyis the reference. (Minor pre-existing note:ReadAudioChunkedcould spin if a non-final chunk short-reads — low priority.)https://claude.ai/code/session_01UrSHLdLaod5Uih8VuoK4eJ
Summary by CodeRabbit
Documentation
Bug Fixes
Tests