Skip to content

perf: eliminate hot-path allocations, stream AAC incrementally, close WAV/Cut gaps - #1

Merged
cloud-hai-vo merged 3 commits into
mainfrom
perf/realtime-streaming-and-gaps
Aug 5, 2026
Merged

perf: eliminate hot-path allocations, stream AAC incrementally, close WAV/Cut gaps#1
cloud-hai-vo merged 3 commits into
mainfrom
perf/realtime-streaming-and-gaps

Conversation

@cloud-hai-vo

Copy link
Copy Markdown
Contributor

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.

  • AAC streaming fix (the big one): AacEncoderSession previously buffered the entire track into a List<short> and only ran the encoder once, at Finish() — 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 shared AacFrameEncoder engine, so both the streaming session and the whole-buffer AacEncoder.Encode API produce identical output.
  • Hot-path allocations eliminated: WavReader/WavWriter no longer allocate a fresh byte[] 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); AudioCutter no longer does .ToArray() copies per decoded block; BitWriter (AAC's hottest loop) no longer does List<byte>.Add() per output bit.
  • Feature gaps closed: WAV gains 8-bit unsigned and 32-bit IEEE-float PCM support (previously 16/24-bit only). AudioCutter.Cut now 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.
  • Benchmarks: added 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.Cut no longer throws NotSupportedException on mismatched source/dest extensions — it performs the cross-format cut instead. Flagged via perf!: 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 Release passes on all three target frameworks (107 passed, 1 pre-existing skip)
  • New tests added for WAV 8-bit/32-bit-float round trip, reused-buffer correctness, cross-format cut, and AAC session incremental-flush/fail-fast validation
  • Benchmark project builds and runs

🤖 Generated with Claude Code

…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.
@cloud-hai-vo
cloud-hai-vo merged commit aa470bb into main Aug 5, 2026
3 checks passed
@cloud-hai-vo
cloud-hai-vo deleted the perf/realtime-streaming-and-gaps branch August 5, 2026 03:53
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