perf: eliminate hot-path allocations, stream AAC incrementally, close WAV/Cut gaps - #1
Merged
Merged
Conversation
…CM formats - WavReader/WavWriter now reuse a grow-only scratch buffer instead of allocating a fresh byte[] on every ReadInterleavedSamples/ WriteInterleavedSamples call, removing the dominant GC-pressure source on every codec's probe/convert/cut path (all of them bottleneck through WAV I/O). - AudioCutter no longer allocates a fresh int[] per decoded block (block.ToArray() / slice.ToArray()); reuses a single grow-only buffer across the whole operation instead. - Add 8-bit unsigned and 32-bit IEEE-float PCM support to WavReader, and 8-bit/32-bit PCM output to WavWriter (previously 16/24-bit only). - Generalize AudioCutter.Cut to decode any supported source and encode to any supported destination (previously required source and dest extensions to match, and WMA wasn't a supported Cut source at all). BREAKING CHANGE: AudioCutter.Cut no longer requires destExtension to match sourceExtension; callers relying on the NotSupportedException for mismatched extensions will instead get a successful cross-format cut.
- AacEncoderSession previously accumulated every sample into a List<short> and only ran the encoder once, at Finish() -- meaning it produced zero output and held the entire track in memory until the session closed. That's the opposite of what a real-time/streaming pipeline needs. Extract the per-frame encode loop from AacEncoder into a reusable AacFrameEncoder that emits an ADTS frame to the output stream as soon as each 1024-sample block completes, and have both AacEncoder.Encode (whole-buffer convenience API) and AacEncoderSession (streaming API) drive the same engine, so behavior/output stays identical between the two. - AacEncoderSession.OpenSession now validates the sample rate immediately (previously deferred until Finish(), i.e. after the whole track had already been buffered). - BitWriter's List<byte> bit-by-bit backing (Add() call per output bit, the hottest loop in AAC encoding) replaced with a plain grow-on-demand byte[].
…ocations Adds EggEncoder.Benchmarks (excluded from packing/publish, added to the solution for discoverability/CI build coverage only) covering: - WavReader/WavWriter block read/write throughput and allocations - AudioCutter.Convert end-to-end WAV->MP3/FLAC throughput - AacEncoderSession incremental streaming throughput Run with: dotnet run -c Release --project src/EggEncoder.Benchmarks Confirms the preceding allocation fixes: reading 10s of stereo 16-bit WAV in 4096-frame blocks now allocates ~20KB total instead of scaling with block count.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full pass over the codec toolkit focused on real-time/streaming viability (target use case: low-latency streaming pipelines, so GC pressure and buffering behavior matter as much as raw throughput) plus a few bounded feature gaps.
AacEncoderSessionpreviously buffered the entire track into aList<short>and only ran the encoder once, atFinish()— meaning zero output and unbounded memory until the session closed, the opposite of streaming. It now emits an ADTS frame incrementally every 1024 samples via a sharedAacFrameEncoderengine, so both the streaming session and the whole-bufferAacEncoder.EncodeAPI produce identical output.WavReader/WavWriterno longer allocate a freshbyte[]on every block (previously the dominant GC-pressure source across every codec's probe/convert/cut path, since they all bottleneck through WAV I/O);AudioCutterno longer does.ToArray()copies per decoded block;BitWriter(AAC's hottest loop) no longer doesList<byte>.Add()per output bit.AudioCutter.Cutnow supports any decodable source → any encodable destination — previously it required matching source/dest extensions and didn't support WMA as a cut source at all.EggEncoder.Benchmarks(BenchmarkDotNet) covering WAV I/O, end-to-end convert throughput, and AAC streaming, so perf claims are measurable going forward. Confirmed: reading 10s of stereo 16-bit WAV in 4096-frame blocks now allocates ~20KB total instead of scaling with block count.Breaking change
AudioCutter.Cutno longer throwsNotSupportedExceptionon mismatched source/dest extensions — it performs the cross-format cut instead. Flagged viaperf!:in the commit.Explicitly out of scope
A full WMA encoder, MP4/MOV audio decode, and several AAC/WMA decoder edge cases (TNS, prediction, intensity/mid-side stereo, bit-reservoir) are not attempted here — each is genuine multi-day proprietary-bitstream or DSP work needing dedicated fixture-based correctness validation that doesn't exist yet. Left as clear
NotSupportedExceptions rather than risking silently-corrupt audio output.Test plan
dotnet build --configuration Release(net8.0/net9.0/net10.0)dotnet test --configuration Releasepasses on all three target frameworks (107 passed, 1 pre-existing skip)🤖 Generated with Claude Code