Skip to content

feat: add MP3 streaming encoder (item 8's MP3 sub-item) - #120

Merged
cloud-hai-vo merged 3 commits into
mainfrom
feat/mp3-streaming-encoder
Oct 17, 2026
Merged

cloud-hai-vo merged 3 commits into
mainfrom
feat/mp3-streaming-encoder

Conversation

@cloud-hai-vo

Copy link
Copy Markdown
Contributor

Summary

  • Implements the MP3 sub-item of docs/streaming-plan.md item 8: Mp3StreamingEncoder is the third IStreamingEncoder implementation, reusing Mp3FrameEncoder's own encode internals unchanged — only how each encoded frame is delivered differs (a callback emitting raw, self-delimiting MPEG frames instead of writing bytes to a file).
  • Writes no dedicated "Info tag" (LAME-tag) header frame and no ID3v2 tag at all — both are container/file-level conventions with no equivalent for a raw, open-ended frame stream, the same reasoning that already excluded ADTS framing from AacStreamingEncoder's own raw access units (PR feat: add push-style IStreamingEncoder + AAC-LC streaming session #110) and Ogg/pre_skip from OpusStreamingEncoder's own raw packets (PR feat: add Opus streaming encoder (item 8's Opus sub-item) #118). Mp3Encoder.EncoderDelaySamples (the existing, already empirically-measured 1680) remains the right value for a muxer here to use directly.
  • Unlike OpusStreamingEncoder, this encoder reports each frame's own real sample count rather than always the nominal 1152 — tracking it costs nothing extra here, the same way it doesn't for AacStreamingEncoder, since Mp3EncoderSession.Finish's own existing design already computes the real remaining count before padding it.
  • Complete()'s own mandatory flush frame (draining the one-granule MDCT history — the same real, previously-found-and-fixed requirement Mp3EncoderSession.Finish itself documents) is purely synthetic and reports a real sample count of 0.
  • FLAC streaming sessions (item 8's remaining sub-item) remain not started.
  • Docs synced: docs/streaming-plan.md (MP3 sub-item marked done), CLAUDE.md, README.md, llms.txt, llms-full.txt.

Test plan

  • New Mp3StreamingEncoderTest: construction validation (null callback, unsupported channels/sample rate/bitrate), SamplesPerFrame == 1152, Complete() with zero samples emits nothing, idempotent Complete()/safe double Dispose(), Write after Complete() throws, monotonic frame indices, exact per-frame SampleCount verification (real count for the padded tail frame, 0 for the synthetic flush frame — including the "exactly one full frame, nothing pending" edge case), real round-trip SNR for mono and stereo (concatenating raw frames into a file, decoding via the project's own Mp3Decoder/NLayer), and a regression test mirroring Mp3EncoderSnrTest's own exact-tail-preservation check, confirming this restructured callback-based version of that fix still holds.
  • Full existing suite (dotnet test, net10.0 locally): 1703 passed, 1 pre-existing unrelated skip, 0 failed.
  • Full solution build (all target frameworks + AotSmokeTest): 0 warnings/errors.

🤖 Generated with Claude Code

cloud-hai-vo and others added 3 commits October 17, 2026 17:14
Implements the MP3 sub-item of docs/streaming-plan.md item 8:
Mp3StreamingEncoder is the third IStreamingEncoder implementation,
reusing Mp3FrameEncoder's own encode internals unchanged -- only how
each encoded frame is delivered differs (a callback emitting raw,
self-delimiting MPEG frames instead of writing bytes to a file).

Writes no dedicated "Info tag" (LAME-tag) header frame and no ID3v2
tag at all -- both are container/file-level conventions with no
equivalent for a raw, open-ended frame stream, the same reasoning
that already excluded ADTS framing from AacStreamingEncoder's own
raw access units and Ogg/pre_skip from OpusStreamingEncoder's own raw
packets. Mp3Encoder.EncoderDelaySamples (the existing, already
empirically-measured 1680) remains the right value for a muxer here
to use directly.

Unlike OpusStreamingEncoder, this encoder reports each frame's own
real sample count rather than always the nominal 1152 -- tracking it
costs nothing extra here, the same way it doesn't for
AacStreamingEncoder, since Mp3EncoderSession.Finish's own existing
design already computes the real remaining count before padding it.
Complete()'s own mandatory flush frame (draining the one-granule MDCT
history, the same real, previously-found-and-fixed requirement
Mp3EncoderSession.Finish itself documents) is purely synthetic and
reports 0.

FLAC streaming sessions (item 8's remaining sub-item) remain not
started.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Second self-review pass: both call sites always passed the same
FrameSamples constant, so the parameter added indirection with no
actual flexibility.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cloud-hai-vo
cloud-hai-vo merged commit cf30659 into main Oct 17, 2026
9 checks passed
@cloud-hai-vo
cloud-hai-vo deleted the feat/mp3-streaming-encoder branch October 17, 2026 10:20
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