Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ ordered plan, acceptance criteria per item, and what's merged versus still open.
detecting drift at all. Reads `Channels`/`SampleRate` directly from the wrapped encoder, so the
pump can never disagree with it about either value.
- `Streaming.Opus.OpusStreamingEncoder` (`Streaming/Opus/`) -- the second `IStreamingEncoder`
implementation (item 8's own Opus sub-item; FLAC still open), same scope as
implementation (item 8's own Opus sub-item), same scope as
`Codecs.Opus.OpusEncoderSession` (fixed 48kHz, mono/stereo, 16-bit PCM), reusing that session's
own Concentus-backed encode internals unchanged. Unlike `AacStreamingEncoder`,
`EncoderDelaySamples` is a per-instance property reading the underlying encoder's own
Expand All @@ -207,7 +207,7 @@ ordered plan, acceptance criteria per item, and what's merged versus still open.
nothing remains pending afterward -- that exists in the file-based session purely to flag an Ogg
page's own end-of-stream bit, a container-level concept this raw-packet encoder has none of.
- `Streaming.Mp3.Mp3StreamingEncoder` (`Streaming/Mp3/`) -- the third `IStreamingEncoder`
implementation (item 8's own MP3 sub-item; FLAC still open), same scope as
implementation (item 8's own MP3 sub-item), same scope as
`Codecs.Mp3.Mp3EncoderSession` (mono/stereo, 32000/44100/48000Hz, CBR only), reusing
`Mp3FrameEncoder`'s own encode internals unchanged. Writes no dedicated "Info tag" (LAME-tag)
header frame and no ID3v2 tag -- both are container/file-level conventions with no equivalent
Expand All @@ -222,6 +222,25 @@ ordered plan, acceptance criteria per item, and what's merged versus still open.
of `0`. Since MP3 frames are self-delimiting with no container needed at all, the streaming
encoder's own raw frames are simply concatenated directly into a file for conformance testing,
decoded through the project's own public `Mp3Decoder` (NLayer).
- `Streaming.Flac.FlacStreamingEncoder` (`Streaming/Flac/`) -- the fourth and last
`IStreamingEncoder` implementation (item 8's own FLAC sub-item, completing it), same scope as
`Codecs.Flac.FlacEncoderSession` (any channel count, 1-32 bits per sample), reusing
`FlacFrameEncoder`'s own encode internals unchanged. Genuinely simpler than the other three, for
two real codec-level reasons: this encoder's own FIXED predictors are purely causal (never look
at "future" samples), so there is no lookahead to drain at all (`EncoderDelaySamples` is always
`0`); and FLAC's own frame format natively allows a final block shorter than the nominal size
with no padding, unlike AAC-LC/Opus/MP3's own fixed-or-padded sizes -- `Complete()` just flushes
whatever's pending, no mandatory extra frame. **Genuinely not independently decodable**, unlike
the other three: `FlacFrameEncoder`'s own existing frame header always defers sample-rate and
bits-per-sample to STREAMINFO (a pre-existing characteristic of this project's frame encoder, not
introduced here) -- building a STREAMINFO (a whole-stream, container-level concept) is out of
scope for this raw packet-level encoder, the same boundary every other `IStreamingEncoder` already
draws. `FlacEncoderSession.WriteStreamInfoPayload` was widened from `private` to `internal` (zero
behavior change) so this item's own conformance tests can build a real STREAMINFO out-of-band and
reuse that exact bit layout rather than duplicating it. Conformance: unlike the lossy AAC/Opus/MP3
sub-items' own SNR-floor checks, this verifies EXACT sample equality after a full decode (via the
project's own `FlacDecoder`) -- the same bar `docs/managed-codec-rewrite-plan.md` already holds
every lossless codec in this project to.

### Native binary packaging

Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Covers **resampling** (`ResamplingTransform`), **gain / peak normalization** (`V

## Live Streaming (early)

A push-style, incremental HLS packaging pipeline, genuinely independent of the `IMediaEncoder` file/`Stream` surface above. `IStreamingEncoder` encodes PCM into frames via a callback; a fragmented MP4 (fMP4/CMAF) muxer or an MPEG-TS muxer packages frames into segments; `HlsSegmenter` decides segment boundaries and writes segments + an `.m3u8` playlist through an `IStreamingSink`, in either VOD mode (every segment stays in the playlist) or live mode (a bounded sliding window of the most recent segments, old ones reclaimed via the sink). A top-level master playlist (`M3u8MasterPlaylistWriter`) lists multiple audio-bitrate renditions for a player to choose from. `LiveAudioPump` feeds PCM from a genuinely live source (not neat, exactly-on-time blocks) into an encoder, with an optional silence-fill policy for real gaps in arrival. AAC-LC (`AacStreamingEncoder`), Opus (`OpusStreamingEncoder`), and MP3 (`Mp3StreamingEncoder`) all implement `IStreamingEncoder`; the muxer/segmenter/playlist layers above are AAC-LC-only so far — see [docs/streaming-plan.md](docs/streaming-plan.md) for the full ordered plan and what's merged versus still open.
A push-style, incremental HLS packaging pipeline, genuinely independent of the `IMediaEncoder` file/`Stream` surface above. `IStreamingEncoder` encodes PCM into frames via a callback; a fragmented MP4 (fMP4/CMAF) muxer or an MPEG-TS muxer packages frames into segments; `HlsSegmenter` decides segment boundaries and writes segments + an `.m3u8` playlist through an `IStreamingSink`, in either VOD mode (every segment stays in the playlist) or live mode (a bounded sliding window of the most recent segments, old ones reclaimed via the sink). A top-level master playlist (`M3u8MasterPlaylistWriter`) lists multiple audio-bitrate renditions for a player to choose from. `LiveAudioPump` feeds PCM from a genuinely live source (not neat, exactly-on-time blocks) into an encoder, with an optional silence-fill policy for real gaps in arrival. AAC-LC (`AacStreamingEncoder`), Opus (`OpusStreamingEncoder`), MP3 (`Mp3StreamingEncoder`), and FLAC (`FlacStreamingEncoder`) all implement `IStreamingEncoder`; the muxer/segmenter/playlist layers above are AAC-LC-only so far — see [docs/streaming-plan.md](docs/streaming-plan.md) for the full ordered plan and what's merged versus still open.

```csharp
using EggEncoder.Streaming;
Expand Down Expand Up @@ -173,6 +173,18 @@ using var mp3Encoder = Mp3StreamingEncoder.Create(channels: 1, sampleRate: 44100
}, bitRateKbps: 192);
```

`FlacStreamingEncoder` is the fourth and last, lossless, with no encoder lookahead at all -- but its raw frames, unlike the other three, are not independently decodable without a real STREAMINFO built out-of-band (a container-level concept this raw encoder deliberately leaves out of scope):

```csharp
using EggEncoder.Streaming.Flac;

using var flacEncoder = FlacStreamingEncoder.Create(channels: 1, sampleRate: 44100, bitsPerSample: 16, frame =>
{
// frame.SampleCount reports the real sample count for every frame, including a genuinely
// short final one -- FLAC needs no padding at all, unlike AAC-LC/Opus/MP3
});
```

## Supported Formats

| Format | Probe | Decode | Encode |
Expand Down
34 changes: 30 additions & 4 deletions docs/streaming-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,10 +845,36 @@ lands:
real audio, not just an approximately-right duration.
- **FLAC**: lossless streaming is a real but lower-demand case for a *live-streaming* plan
specifically (FLAC-over-HLS is uncommon in practice vs. AAC/Opus/MP3); worth doing once the
pattern is proven three times over, not before. Not started.

**Status**: partially done -- Opus and MP3 sub-items complete (see above), FLAC sub-item not
started.
pattern is proven three times over, not before. **Done** ([PR #121](https://github.com/eggspot/EggEncoder/pull/121)) --
`Streaming.Flac.FlacStreamingEncoder` (`src/EggEncoder/Streaming/Flac/`), same scope as
`FlacEncoderSession` (any channel count, 1-32 bits per sample), reusing `FlacFrameEncoder`'s own
encode internals unchanged. Genuinely simpler than the other three sub-items, for two real,
codec-level reasons rather than a shortcut: this encoder's own FIXED predictors are purely
causal (only ever look at already-encoded samples within the same block, never "future" ones),
so there is no encoder lookahead to drain at all (`EncoderDelaySamples` is `0`, not merely
omitted); and FLAC's own frame format natively allows a final block shorter than the nominal
size with no padding, unlike AAC-LC/Opus/MP3's own fixed-or-padded frame sizes -- `Complete()`
therefore just flushes whatever's left pending, nothing more, no mandatory extra frame.
**Genuinely not independently decodable, unlike the other three sub-items**: `FlacFrameEncoder`'s
own existing frame header (unchanged here) always defers sample-rate and bits-per-sample to
STREAMINFO (writes code `0` for both, rather than using FLAC's own available explicit escape
codes) -- a real, pre-existing characteristic of this project's frame encoder, not introduced by
this item. Building a STREAMINFO (a whole-stream, container-level concept -- total duration, a
whole-stream MD5, min/max block size) is out of scope here, the same "container concerns belong
to a downstream muxer" boundary every other `IStreamingEncoder` already draws; a caller needing a
real, playable FLAC stream must construct one itself from `Channels`/`SampleRate`/the
`bitsPerSample` it already passed to `Create`. `FlacEncoderSession.WriteStreamInfoPayload` was
widened from `private` to `internal` (zero behavior change, confirmed by the full existing
`FlacEncoderTest` suite still passing unchanged) so this item's own conformance tests can build a
real STREAMINFO out-of-band and reuse the exact same bit layout rather than duplicating it.
Conformance: unlike the lossy AAC/Opus/MP3 sub-items' own SNR-floor checks, this verifies EXACT
sample equality after a full decode (via the project's own `FlacDecoder`) of a real file built
from the streaming encoder's own raw frames plus that out-of-band STREAMINFO -- the same "exact
original samples reproduced" bar `docs/managed-codec-rewrite-plan.md` already holds every
lossless codec in this project to.

**Status**: done -- Opus, MP3, and FLAC sub-items all complete (see above). Item 8 is now fully
done.

## Top-level test strategy (applies across every item above)

Expand Down
37 changes: 35 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -999,8 +999,8 @@ public sealed class Mp3StreamingEncoder : IStreamingEncoder
}
```

The third `IStreamingEncoder` implementation (item 8's own MP3 sub-item; FLAC sub-item still
open), reusing `Mp3FrameEncoder`'s own encode internals unchanged -- only how each encoded frame
The third `IStreamingEncoder` implementation (item 8's own MP3 sub-item), reusing
`Mp3FrameEncoder`'s own encode internals unchanged -- only how each encoded frame
is delivered differs. Writes no dedicated "Info tag" (LAME-tag) header frame and no ID3v2 tag at
all -- both are container/file-level conventions (gapless-playback metadata meant to be read back
from a complete, seekable file) with no equivalent for a raw, open-ended frame stream.
Expand All @@ -1020,6 +1020,39 @@ technique `Mp3EncoderSnrTest`'s own regression test for this exact fix already e
this restructured, callback-based version of the flush-frame fix still preserves the true tail of
real audio.

```csharp
public sealed class FlacStreamingEncoder : IStreamingEncoder
{
public const int EncoderDelaySamples = 0; // FIXED predictors are purely causal -- no lookahead to drain, unlike AAC-LC/Opus/MP3

public static IStreamingEncoder Create(int channels, int sampleRate, int bitsPerSample, EncodedAudioFrameCallback onFrameEncoded);
// throws ArgumentNullException for a null callback, NotSupportedException for channels < 1
// or bitsPerSample outside 1-32 -- the same scope as Codecs.Flac.FlacEncoderSession
}
```

The fourth and last `IStreamingEncoder` implementation (item 8's own FLAC sub-item, completing
it), reusing `FlacFrameEncoder`'s own encode internals unchanged. Genuinely simpler than the other
three, for two real codec-level reasons: FIXED predictors are purely causal (never look at
"future" samples), so there is no lookahead to drain; and FLAC's own frame format natively allows
a final block shorter than the nominal size with no padding at all, unlike AAC-LC/Opus/MP3's own
fixed-or-padded sizes -- `Complete()` therefore just flushes whatever's left pending, no mandatory
extra frame. **Genuinely not independently decodable**, unlike the other three:
`FlacFrameEncoder`'s own existing frame header always defers sample-rate and bits-per-sample to
STREAMINFO (writes code `0` for both -- a real, pre-existing characteristic of this project's
frame encoder, not introduced here) rather than using FLAC's own available explicit escape codes.
Building a STREAMINFO (a whole-stream, container-level concept: total duration, a whole-stream
MD5, min/max block size) is out of scope for this raw packet-level encoder, the same "container
concerns belong to a downstream muxer" boundary every other `IStreamingEncoder` already draws -- a
caller needing a real, playable FLAC stream must construct one itself from `Channels`/
`SampleRate`/the `bitsPerSample` it already passed to `Create`.
`Codecs.Flac.FlacEncoderSession.WriteStreamInfoPayload` was widened from `private` to `internal`
(zero behavior change) so this item's own conformance tests can build a real STREAMINFO
out-of-band and reuse that exact bit layout rather than duplicating it. Conformance: unlike the
lossy AAC/Opus/MP3 sub-items' own SNR-floor checks, this verifies EXACT sample equality after a
full decode (via the project's own `FlacDecoder`) -- the same bar every lossless codec in this
project is already held to.

## AudioCutter — `EggEncoder.Codecs`

Format-dispatching convert/cut/mix/concatenate used internally by `NativeEncoder`; also usable directly:
Expand Down
12 changes: 11 additions & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ lookahead isn't fixed at one project-wide value the way AAC-LC's is), and every
`SampleCount` is always `960` -- Opus has no bitstream-level short-final-frame concept.

`EggEncoder.Streaming.Mp3.Mp3StreamingEncoder` is the third `IStreamingEncoder` implementation
(item 8's MP3 sub-item; FLAC still open), same scope as `Codecs.Mp3.Mp3EncoderSession`
(item 8's MP3 sub-item), same scope as `Codecs.Mp3.Mp3EncoderSession`
(mono/stereo, 32000/44100/48000Hz, CBR only), reusing `Mp3FrameEncoder`'s own encode internals.
Writes no LAME-tag header frame or ID3v2 tag (both are container/file-level conventions with no
equivalent for a raw frame stream); `Mp3Encoder.EncoderDelaySamples` (`1680`) remains the right
Expand All @@ -244,6 +244,16 @@ count (like `AacStreamingEncoder`) rather than always the nominal `1152` -- exce
own mandatory flush frame (draining the one-granule MDCT history), which is purely synthetic and
reports `0`.

`EggEncoder.Streaming.Flac.FlacStreamingEncoder` is the fourth and last `IStreamingEncoder`
implementation (item 8's FLAC sub-item, completing it), same scope as
`Codecs.Flac.FlacEncoderSession` (any channel count, 1-32 bits per sample), reusing
`FlacFrameEncoder`'s own encode internals. Simpler than the other three: FIXED predictors are
purely causal (no lookahead to drain -- `EncoderDelaySamples` is always `0`), and FLAC's own frame
format allows a short final block with no padding at all, so `Complete()` just flushes whatever's
pending. Genuinely **not** independently decodable (unlike the other three): its frame header
defers sample-rate/bits-per-sample to STREAMINFO, a whole-stream container concept this raw
encoder leaves out of scope -- a caller needing a real FLAC stream must build one itself.

## MP3 ID3v2 Tags

`Probe`-ing an MP3 file with an ID3v2.3/2.4 tag populates the same `ProbeResult.Tags` WAV's own
Expand Down
Loading
Loading