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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dotnet test src/EggEncoder.UnitTests/EggEncoder.UnitTests.csproj --configuration
| `Flac/` | `FlacDecoder`/`FlacEncoder` — thin wrappers over native `libFLAC` P/Invoke bindings |
| `Mp3/` | `Mp3Decoder` (via the `NLayer` managed decoder), `Mp3Encoder` (native `libmp3lame` P/Invoke), `Mp3Probe` (manual frame-header parsing, no native call) |
| `Wav/` | `WavReader`/`WavWriter` — RIFF/WAVE PCM I/O, the common source/sink format all codecs read from or write to |
| `Wma/` | `WmaDecoder` + `AsfContainerReader` (ASF/WMA container parsing) + `WmaTables` |
| `Wma/` | Pure managed WMAv2 decoder/encoder (`WmaDecoder`, `WmaEncoder`, `WmaEncoderSession`, `WmaFrameEncoder`) + `AsfContainerReader`/`AsfContainerWriter` (ASF/WMA container I/O) + `WmaTables` |
| `Mov/` | `MovProbe` — MOV/MP4 atom-tree walker for metadata-only probing (no audio decode) |
| `AudioCutter.cs` | Format-dispatching `Convert`/`Cut` used by `NativeEncoder`; defines the internal `IAudioSink` interface implemented by each codec's writer/session type |

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🥚 EggEncoder

> **Audio encoding/decoding toolkit for .NET** — native MP3/FLAC codec bindings, managed AAC/WMA decode, and built-in waveform generation, all behind one `IMediaEncoder` interface.
> **Audio encoding/decoding toolkit for .NET** — native MP3/FLAC codec bindings, managed AAC/WMA encode/decode, and built-in waveform generation, all behind one `IMediaEncoder` interface.

Sponsored by [eggspot.app](https://eggspot.app)

Expand Down Expand Up @@ -72,7 +72,7 @@ var probeResult = await encoder.Probe("track.flac");
| FLAC | ✅ | ✅ | ✅ |
| MP3 | ✅ | ✅ | ✅ |
| AAC | ✅ | ✅ | ✅ |
| WMA | ✅ | ✅ | |
| WMA | ✅ | ✅ | |
| MOV/MP4 (metadata only) | ✅ | ❌ | ❌ |

`IMediaEncoder.CutFile` decodes any supported source (WAV, FLAC, MP3, AAC, WMA) and can cut into any supported destination format, including converting as it trims — sample-accurate, no re-encode of the untouched region.
Expand Down
7 changes: 6 additions & 1 deletion docs/API-Reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,13 @@ <h3>WMA — <code>EggEncoder.Codecs.Wma</code></h3>
<pre><code class="language-csharp">public static class WmaDecoder
{
public static WmaStreamInfo Decode(string wmaFilePath, AudioBlockDecodedCallback onBlockDecoded);
}

public static class WmaEncoder
{
public static void Encode(string destFilePath, IReadOnlyList&lt;short&gt; interleavedSamples, int channels, int sampleRate);
}</code></pre>
<p>Decode-only — mono only (mid/side stereo coding, used by real-world WMAv2 encoders including ffmpeg's, is not supported and throws <code>NotSupportedException</code>).</p>
<p>Mono or independently-coded stereo only. Mid/side stereo coding (used by most real-world WMAv2 encoders, including ffmpeg's) is not supported for decode and throws <code>NotSupportedException</code>; <code>WmaEncoder</code> never produces mid/side output, so this library's own encoder/decoder round-trip always works.</p>

<h3>MOV/MP4 — <code>EggEncoder.Codecs.Mov</code></h3>
<pre><code class="language-csharp">public static class MovProbe
Expand Down
2 changes: 1 addition & 1 deletion docs/Advanced-Features.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ <h2>Waveform Generation</h2>
<p><code>WaveformCalculator</code> streams — call <code>AddBlock</code> as many times as you like across however many decode callbacks the codec produces; it does not need the full signal in memory at once.</p>

<h2>Sample-Accurate Cutting</h2>
<p><code>NativeEncoder.CutFile</code> (backed by <code>AudioCutter.Cut</code>) decodes any supported source (WAV, FLAC, MP3, AAC, WMA) and writes any supported destination format (WAV, FLAC, MP3, AAC) without re-encoding the untouched region where the format allows frame-boundary slicing:</p>
<p><code>NativeEncoder.CutFile</code> (backed by <code>AudioCutter.Cut</code>) decodes any supported source (WAV, FLAC, MP3, AAC, WMA) and writes any supported destination format (WAV, FLAC, MP3, AAC, WMA) without re-encoding the untouched region where the format allows frame-boundary slicing:</p>
<pre><code class="language-csharp">using EggEncoder.Codecs;

bool produced = AudioCutter.Cut(sourcePath, destPath, startInSeconds: 30, endInSeconds: 90);
Expand Down
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home — EggEncoder</title>
<meta name="description" content="EggEncoder — audio encoding/decoding toolkit for .NET. Native MP3/FLAC codec bindings, managed AAC/WMA decode, and built-in waveform generation, behind one IMediaEncoder interface.">
<meta name="description" content="EggEncoder — audio encoding/decoding toolkit for .NET. Native MP3/FLAC codec bindings, managed AAC/WMA encode/decode, and built-in waveform generation, behind one IMediaEncoder interface.">
<link rel="icon" href="favicon.ico">
<link rel="stylesheet" href="assets/css/eggspot.css">
<link rel="stylesheet" href="assets/css/prism-eggspot.css">
Expand Down Expand Up @@ -45,7 +45,7 @@
<section class="hero">
<div class="hero-badge">Free &amp; MIT Licensed</div>
<h1>Audio encode/decode <span class="accent">without the ceremony</span></h1>
<p class="hero-sub">One <code>IMediaEncoder</code> interface, fully native and in-process. Direct P/Invoke bindings to LAME and libFLAC, managed AAC/WMA decode, and built-in waveform generation — no ffmpeg, no subprocess.</p>
<p class="hero-sub">One <code>IMediaEncoder</code> interface, fully native and in-process. Direct P/Invoke bindings to LAME and libFLAC, managed AAC/WMA encode/decode, and built-in waveform generation — no ffmpeg, no subprocess.</p>
<div class="btn-group">
<a class="btn btn-primary" href="Getting-Started.html">Get Started</a>
<a class="btn btn-outline" href="https://github.com/eggspot/EggEncoder" target="_blank" rel="noopener">View on GitHub</a>
Expand Down
20 changes: 13 additions & 7 deletions llms-full.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# EggEncoder — Full Reference

> Audio encoding/decoding toolkit for .NET — native MP3/FLAC codec bindings, managed AAC/WMA decode, and built-in waveform generation, behind one IMediaEncoder interface.
> Audio encoding/decoding toolkit for .NET — native MP3/FLAC codec bindings, managed AAC/WMA encode/decode, and built-in waveform generation, behind one IMediaEncoder interface.

## Overview

Expand Down Expand Up @@ -105,10 +105,10 @@ public class TranscodeWorker(IServiceProvider serviceProvider)
| FLAC | yes | yes | yes |
| MP3 | yes | yes | yes |
| AAC | yes | yes | yes |
| WMA | yes | yes | no |
| WMA | yes | yes | yes |
| MOV/MP4 (metadata only) | yes | no | no |

`AudioCutter.Cut` (used by `NativeEncoder.CutFile`) decodes any supported source format (WAV, FLAC, MP3, AAC, WMA) and writes to any supported destination format, including WAV, FLAC, MP3, or AAC — it can transcode while trimming; source and destination extensions no longer need to match. Sample-accurate, no re-encode of the untouched region. `WmaDecoder` is decode-only and mono-only: real-world WMAv2 stereo encoders (including ffmpeg's) default to mid/side stereo coding, which is unsupported and throws `NotSupportedException`.
`AudioCutter.Cut` (used by `NativeEncoder.CutFile`) decodes any supported source format (WAV, FLAC, MP3, AAC, WMA) and writes to any supported destination format, including WAV, FLAC, MP3, AAC, or WMA — it can transcode while trimming; source and destination extensions no longer need to match. Sample-accurate, no re-encode of the untouched region. `WmaDecoder` supports mono and independently-coded stereo; real-world WMAv2 stereo encoders (including ffmpeg's) default to mid/side stereo coding, which is unsupported and throws `NotSupportedException`. `WmaEncoder` always writes independently-coded channels, so its own output is always decodable by `WmaDecoder`.

## Codec Reference

Expand Down Expand Up @@ -203,11 +203,17 @@ Every other codec's `Convert`/`Cut` path reads from or writes to WAV as the comm
public static class WmaDecoder
{
public static WmaStreamInfo Decode(string wmaFilePath, AudioBlockDecodedCallback onBlockDecoded);
// throws NotSupportedException for stereo (mid/side coding unsupported) or channel counts other than 1/2
// throws NotSupportedException for mid/side-coded stereo or channel counts other than 1/2
}

public static class WmaEncoder
{
public static void Encode(string destFilePath, IReadOnlyList<short> interleavedSamples, int channels, int sampleRate);
// mono or stereo only; always writes independently-coded channels (no mid/side stereo)
}
```

Container parsing via the internal `AsfContainerReader`. Decode-only — there is no `WmaEncoder`.
Container parsing/writing via the internal `AsfContainerReader`/`AsfContainerWriter`. `WmaEncoderSession` implements the same streaming `IAudioSink` used by the other codecs' `AudioCutter` integration.

### MOV/MP4 — `EggEncoder.Codecs.Mov`

Expand Down Expand Up @@ -240,7 +246,7 @@ public static class AudioCutter

public static bool Cut(string sourceFilePath, string destFilePath, int startInSeconds, int endInSeconds);
// decodes any supported source (.wav/.flac/.mp3/.aac/.wma) and writes any supported dest
// extension (.wav/.flac/.mp3/.aac) -- source and dest extensions no longer need to match,
// extension (.wav/.flac/.mp3/.aac/.wma) -- source and dest extensions no longer need to match,
// so Cut can transcode while it trims
// returns false (and writes no file) if the requested range is entirely outside the source duration
}
Expand Down Expand Up @@ -282,7 +288,7 @@ Every codec's `Decode` method streams blocks through the same `AudioBlockDecoded
## Constraints and Gotchas

- **Windows x64 only.** `NativeEncoder` and its codec bindings P/Invoke into bundled `win-x64` binaries. There is no cross-platform build or fallback engine — this library does not run on Linux/macOS/ARM.
- **WMA is decode-only and mono-only.** Genuine stereo WMA files (mid/side coded, the real-world default) throw `NotSupportedException` by design rather than decoding incorrectly.
- **WMA supports mono and independently-coded stereo only.** Genuine mid/side-coded stereo WMA files (the real-world default for most third-party encoders) throw `NotSupportedException` on decode rather than decoding incorrectly; `WmaEncoder` never produces mid/side output, so round-tripping through this library's own encoder/decoder always works.
- **`AudioCutter.Cut` can transcode while it trims.** Source and destination extensions no longer need to match -- it decodes any supported source and writes any supported destination format.
- **AAC encoding is mono-only** and restricted to a fixed set of MPEG-4 sample rates (see `AacTables.SampleRates`); other combinations throw `NotSupportedException`.

Expand Down
6 changes: 3 additions & 3 deletions llms.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# EggEncoder

> Audio encoding/decoding toolkit for .NET — native MP3/FLAC codec bindings, managed AAC/WMA decode, and built-in waveform generation, behind one IMediaEncoder interface.
> Audio encoding/decoding toolkit for .NET — native MP3/FLAC codec bindings, managed AAC/WMA encode/decode, and built-in waveform generation, behind one IMediaEncoder interface.

## What is EggEncoder?

Expand Down Expand Up @@ -61,12 +61,12 @@ var probeResult = await encoder.Probe("track.flac");

## Supported Formats

WAV, FLAC, MP3, AAC, WMA (decode-only, mono-only); MOV/MP4 metadata probing via `MovProbe` (no audio decode).
WAV, FLAC, MP3, AAC, WMA (encode + decode); MOV/MP4 metadata probing via `MovProbe` (no audio decode).

## Constraints

- Native codec binaries are Windows x64 only — there is no cross-platform fallback
- `WmaDecoder` does not support mid/side stereo coding (the default for most real-world WMAv2 encoders) — throws `NotSupportedException` for genuine stereo files
- `WmaDecoder` does not support mid/side stereo coding (the default for most real-world WMAv2 encoders) — throws `NotSupportedException` for genuine stereo files. `WmaEncoder` always writes independently-coded channels, so its own output is always decodable.
- `AudioCutter.Cut` decodes any supported source and can write any supported destination format — it can transcode while trimming, source and destination extensions no longer need to match

## Links
Expand Down
57 changes: 57 additions & 0 deletions src/EggEncoder.UnitTests/Codecs/AudioCutterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using EggEncoder.Codecs.Flac;
using EggEncoder.Codecs.Mp3;
using EggEncoder.Codecs.Wav;
using EggEncoder.Codecs.Wma;
using EggEncoder.UnitTests.TestUtilities;
using FluentAssertions;

Expand Down Expand Up @@ -323,6 +324,62 @@ public void Convert_Mp3ToFlac_Should_Produce_Correct_Duration_And_NonSilent_Outp
}
}

[Fact]
public void Convert_WavToWma_Should_Produce_Correct_Duration_And_NonSilent_Output()
{
var tempDirectory = CreateTempDirectory();

try
{
var destWmaPath = Path.Combine(tempDirectory, "dest.wma");

AudioCutter.Convert(_wavFixturePath, destWmaPath);

var decodedSamples = new List<int>();
var streamInfo = WmaDecoder.Decode(destWmaPath, (block, _, _, _, _) => decodedSamples.AddRange(block.ToArray()));

streamInfo.Channels.Should().Be(2);
streamInfo.SampleRate.Should().Be(44100);
decodedSamples.Should().NotBeEmpty();

var rootMeanSquare = Math.Sqrt(decodedSamples.Average(sample => (double)sample * sample));
rootMeanSquare.Should().BeGreaterThan(1000, $"expected a real, non-silent decoded signal, got RMS={rootMeanSquare}");
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}

[Fact]
public void Cut_WavToWma_Should_Produce_Trimmed_NonSilent_Output()
{
var tempDirectory = CreateTempDirectory();

try
{
var destWmaPath = Path.Combine(tempDirectory, "cut.wma");

var wasCut = AudioCutter.Cut(_wavFixturePath, destWmaPath, startInSeconds: 0, endInSeconds: 1);

wasCut.Should().BeTrue();

var decodedSamples = new List<int>();
var streamInfo = WmaDecoder.Decode(destWmaPath, (block, _, _, _, _) => decodedSamples.AddRange(block.ToArray()));

streamInfo.Channels.Should().Be(2);
streamInfo.SampleRate.Should().Be(44100);
decodedSamples.Should().NotBeEmpty();

var rootMeanSquare = Math.Sqrt(decodedSamples.Average(sample => (double)sample * sample));
rootMeanSquare.Should().BeGreaterThan(1000, $"expected a real, non-silent decoded signal, got RMS={rootMeanSquare}");
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}

[Fact]
public void Convert_UnsupportedExtension_Should_Throw()
{
Expand Down
Loading
Loading