From 2ee20d0c2e5220e13a6cceeef5de7b250e8118de Mon Sep 17 00:00:00 2001 From: Hai Vo Date: Wed, 5 Aug 2026 13:28:25 +0700 Subject: [PATCH] feat!: return waveform peaks as a typed list instead of a JSON string ProbeResult.WaveformResult was a string holding a JSON-serialized List -- a leftover of the old JSON-blob-everything design that Result (removed earlier this cycle) also used. Return the data itself: ProbeResult.Waveform is now IReadOnlyList?, populated directly from WaveformCalculator.GetNormalizedWindows() with no serialize step in between. Docs (llms.txt, llms-full.txt, API-Reference.html, Advanced-Features.html, Getting-Started.html) updated to match. BREAKING CHANGE: ProbeResult.WaveformResult (string?) no longer exists; use ProbeResult.Waveform (IReadOnlyList?) instead. --- CLAUDE.md | 2 +- docs/API-Reference.html | 2 +- docs/Advanced-Features.html | 4 ++-- docs/Getting-Started.html | 2 +- llms-full.txt | 2 +- llms.txt | 2 +- src/EggEncoder.UnitTests/NativeEncoderTest.cs | 19 ++++++++----------- src/EggEncoder/NativeEncoder.cs | 12 ++++++------ src/EggEncoder/Results/ProbeResult.cs | 2 +- 9 files changed, 22 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 551ba2f..9e087e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ dotnet test src/EggEncoder.UnitTests/EggEncoder.UnitTests.csproj --configuration - **`Transform/`** — `BitReader`, `BitWriter`, `HuffmanTable`, `Mdct` — low-level bitstream and signal-processing primitives shared by the AAC/WMA codecs - **`Native/`** — `FlacNative.cs`/`Mp3Native.cs` (`[LibraryImport]` P/Invoke declarations), `NativeLibraryLoader.cs` (a `[ModuleInitializer]` that registers a custom `DllImportResolver` so `libFLAC`/`libmp3lame` load from `Native/win-x64/` relative to `AppContext.BaseDirectory` regardless of the consuming app's working directory) -- **`Waveform/WaveformCalculator.cs`** — streaming peak-window calculator fed blocks during decode, used by every codec's probe path to produce `ProbeResult.WaveformResult` +- **`Waveform/WaveformCalculator.cs`** — streaming peak-window calculator fed blocks during decode, used by every codec's probe path to produce `ProbeResult.Waveform` - **`Results/ProbeResult.cs`** — the public `ProbeResult` DTO returned by every `Probe` call - **`ServiceCollectionExtensions.cs`** — `AddEggEncoder()` DI registration; no options, registers `IMediaEncoder` → `NativeEncoder` (scoped) diff --git a/docs/API-Reference.html b/docs/API-Reference.html index 07d5dff..fef4745 100644 --- a/docs/API-Reference.html +++ b/docs/API-Reference.html @@ -72,7 +72,7 @@

ProbeResult

public string? TimeBase { get; init; } // e.g. "1/44100" public int? Width { get; init; } // set for video containers (MOV/MP4), null for audio public int? Height { get; init; } - public string? WaveformResult { get; set; } + public IReadOnlyList<double>? Waveform { get; init; } // normalized peak windows in [0,1], or null }

NativeEncoder

diff --git a/docs/Advanced-Features.html b/docs/Advanced-Features.html index c4eb947..cd9ec19 100644 --- a/docs/Advanced-Features.html +++ b/docs/Advanced-Features.html @@ -43,7 +43,7 @@

Advanced Features

Waveform Generation

-

Every Probe call that decodes audio (WAV, FLAC, MP3, AAC, WMA — not MOV/MP4) populates ProbeResult.WaveformResult with a JSON array of normalized peak values in [0, 1], one per window, via WaveformCalculator:

+

Every Probe call that decodes audio (WAV, FLAC, MP3, AAC, WMA — not MOV/MP4) populates ProbeResult.Waveform with a list of normalized peak values in [0, 1], one per window, via WaveformCalculator:

using EggEncoder.Waveform;
 
 var calculator = new WaveformCalculator(totalSamplesPerChannel, channels, bitsPerSample);
@@ -71,7 +71,7 @@ 

MOV/MP4 Probing

var result = MovProbe.Probe("clip.mp4"); Console.WriteLine($"{result.Width}x{result.Height}, {result.DurationInSeconds}s, {result.CodecFourCc}");
-

This is metadata-only — MovProbe does not decode audio or video frames. NativeEncoder.Probe routes .mov/.mp4 files here automatically and leaves ProbeResult.WaveformResult null.

+

This is metadata-only — MovProbe does not decode audio or video frames. NativeEncoder.Probe routes .mov/.mp4 files here automatically and leaves ProbeResult.Waveform null.

Native Binary Packaging

libmp3lame.dll and libFLAC.dll are packed via the NuGet contentFiles convention so they land at Native/win-x64/*.dll relative to your application's output directory — exactly where NativeLibraryLoader's custom DllImportResolver looks for them at runtime, regardless of your app's working directory. No manual copy step is required after dotnet add package EggEncoder.

diff --git a/docs/Getting-Started.html b/docs/Getting-Started.html index 29e0c8c..491ed44 100644 --- a/docs/Getting-Started.html +++ b/docs/Getting-Started.html @@ -103,7 +103,7 @@

Reading a ProbeResult

public string? TimeBase { get; init; } // e.g. "1/44100" public int? Width { get; init; } // set for video containers (MOV/MP4), null for audio public int? Height { get; init; } - public string? WaveformResult { get; set; } // JSON array of normalized peak windows, or null + public IReadOnlyList<double>? Waveform { get; init; } // normalized peak windows, or null }

See Advanced Features for waveform generation and sample-accurate cutting details, or the API Reference for the full type list.

diff --git a/llms-full.txt b/llms-full.txt index 4fcef59..f1efbd4 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -47,7 +47,7 @@ public class ProbeResult public string? TimeBase { get; init; } // e.g. "1/44100" public int? Width { get; init; } // set for video containers (MOV/MP4), null for audio public int? Height { get; init; } - public string? WaveformResult { get; set; } // JSON array of normalized peak windows in [0,1], or null + public IReadOnlyList? Waveform { get; init; } // normalized peak windows in [0,1], or null } ``` diff --git a/llms.txt b/llms.txt index 41f1907..3a1a3e6 100644 --- a/llms.txt +++ b/llms.txt @@ -55,7 +55,7 @@ var probeResult = await encoder.Probe("track.flac"); - `IMediaEncoder` — the abstraction (`Probe`, `ConvertFile`, `CutFile`) - `NativeEncoder` — the sole implementation, Windows x64 only (P/Invoke to libmp3lame/libFLAC) -- `ProbeResult` — format name/long name, file size, duration, codec name/long name, sample rate, channels, channel layout, bit depth, bitrate, dimensions (for video), waveform JSON +- `ProbeResult` — format name/long name, file size, duration, codec name/long name, sample rate, channels, channel layout, bit depth, bitrate, dimensions (for video), normalized waveform peak windows - `AudioCutter` — format-dispatching convert/cut, used internally by `NativeEncoder` - `WaveformCalculator` — streams decoded blocks into normalized peak windows diff --git a/src/EggEncoder.UnitTests/NativeEncoderTest.cs b/src/EggEncoder.UnitTests/NativeEncoderTest.cs index 4b70b05..c3aa126 100644 --- a/src/EggEncoder.UnitTests/NativeEncoderTest.cs +++ b/src/EggEncoder.UnitTests/NativeEncoderTest.cs @@ -3,7 +3,6 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using System.Text.Json; namespace EggEncoder.UnitTests { @@ -29,7 +28,7 @@ public async Task Probe_WavFile_Should_Return_Correct_Metadata_And_Waveform() { var probeResult = await _nativeEncoder.Probe(_wavFixturePath); - AssertNonEmptyWaveform(probeResult.WaveformResult); + AssertNonEmptyWaveform(probeResult.Waveform); probeResult.FormatName.Should().Be("wav"); probeResult.SizeBytes.Should().Be(new FileInfo(_wavFixturePath).Length); @@ -56,7 +55,7 @@ public async Task Probe_FlacFile_Should_Return_Correct_Metadata_And_Waveform() var probeResult = await _nativeEncoder.Probe(flacPath); - AssertNonEmptyWaveform(probeResult.WaveformResult); + AssertNonEmptyWaveform(probeResult.Waveform); probeResult.FormatName.Should().Be("flac"); probeResult.DurationSeconds.Should().BeApproximately(2, 0.1); @@ -83,7 +82,7 @@ public async Task Probe_Mp3File_Should_Return_Correct_Metadata_And_Waveform() var probeResult = await _nativeEncoder.Probe(mp3Path); - AssertNonEmptyWaveform(probeResult.WaveformResult); + AssertNonEmptyWaveform(probeResult.Waveform); probeResult.FormatName.Should().Be("mp3"); probeResult.DurationSeconds.Should().BeApproximately(2, 0.1); @@ -126,7 +125,7 @@ public async Task Probe_MovFile_Should_Return_Correct_VideoMetadata() { var probeResult = await _nativeEncoder.Probe(_movFixturePath); - probeResult.WaveformResult.Should().BeNull(); + probeResult.Waveform.Should().BeNull(); probeResult.FormatName.Should().Be("mov"); probeResult.DurationSeconds.Should().BeApproximately(5, 0.1); @@ -140,7 +139,7 @@ public async Task Probe_Mp4File_Should_Return_Correct_VideoMetadata() { var probeResult = await _nativeEncoder.Probe(_mp4FixturePath); - probeResult.WaveformResult.Should().BeNull(); + probeResult.Waveform.Should().BeNull(); probeResult.Width.Should().Be(640); probeResult.Height.Should().Be(360); @@ -193,12 +192,10 @@ public async Task CutFile_ToNestedMissingDirectory_Should_CreateDirectory_And_Cu } } - private static void AssertNonEmptyWaveform(string? waveformResult) + private static void AssertNonEmptyWaveform(IReadOnlyList? waveform) { - waveformResult.Should().NotBeNull(); - - var windows = JsonSerializer.Deserialize>(waveformResult!); - windows.Should().NotBeEmpty(); + waveform.Should().NotBeNull(); + waveform.Should().NotBeEmpty(); } private static string CreateTempDirectory() diff --git a/src/EggEncoder/NativeEncoder.cs b/src/EggEncoder/NativeEncoder.cs index cd914b3..6d23c28 100644 --- a/src/EggEncoder/NativeEncoder.cs +++ b/src/EggEncoder/NativeEncoder.cs @@ -127,7 +127,7 @@ private static ProbeResult ProbeWav(string filePath) BitRate = wavReader.SampleRate * wavReader.BitsPerSample * wavReader.Channels, DurationInSamples = wavReader.TotalSamples, TimeBase = wavReader.SampleRate > 0 ? $"1/{wavReader.SampleRate}" : null, - WaveformResult = JsonSerializer.Serialize(waveformCalculator.GetNormalizedWindows()) + Waveform = waveformCalculator.GetNormalizedWindows() }; } @@ -159,7 +159,7 @@ private static ProbeResult ProbeFlac(string filePath) BitRate = streamInfo.SampleRate * streamInfo.BitsPerSample * streamInfo.Channels, DurationInSamples = streamInfo.TotalSamples, TimeBase = streamInfo.SampleRate > 0 ? $"1/{streamInfo.SampleRate}" : null, - WaveformResult = JsonSerializer.Serialize(waveformCalculator?.GetNormalizedWindows() ?? []) + Waveform = waveformCalculator?.GetNormalizedWindows() ?? [] }; } @@ -190,7 +190,7 @@ private static ProbeResult ProbeMp3(string filePath) BitRate = mp3ProbeResult.BitRate, IsVariableBitRate = mp3ProbeResult.IsVariableBitRate, TimeBase = mp3ProbeResult.SampleRate > 0 ? $"1/{mp3ProbeResult.SampleRate}" : null, - WaveformResult = JsonSerializer.Serialize(waveformCalculator?.GetNormalizedWindows() ?? []) + Waveform = waveformCalculator?.GetNormalizedWindows() ?? [] }; } @@ -222,7 +222,7 @@ private static ProbeResult ProbeAac(string filePath) BitRate = streamInfo.SampleRate * streamInfo.BitsPerSample * streamInfo.Channels, DurationInSamples = streamInfo.TotalSamples, TimeBase = streamInfo.SampleRate > 0 ? $"1/{streamInfo.SampleRate}" : null, - WaveformResult = JsonSerializer.Serialize(waveformCalculator?.GetNormalizedWindows() ?? []) + Waveform = waveformCalculator?.GetNormalizedWindows() ?? [] }; } @@ -254,7 +254,7 @@ private static ProbeResult ProbeWma(string filePath) BitRate = streamInfo.SampleRate * streamInfo.BitsPerSample * streamInfo.Channels, DurationInSamples = streamInfo.TotalSamples, TimeBase = streamInfo.SampleRate > 0 ? $"1/{streamInfo.SampleRate}" : null, - WaveformResult = JsonSerializer.Serialize(waveformCalculator?.GetNormalizedWindows() ?? []) + Waveform = waveformCalculator?.GetNormalizedWindows() ?? [] }; } @@ -279,7 +279,7 @@ private static ProbeResult ProbeVideo(string filePath) CodecName = movProbeResult.CodecFourCc, Width = movProbeResult.Width, Height = movProbeResult.Height, - WaveformResult = null + Waveform = null }; } diff --git a/src/EggEncoder/Results/ProbeResult.cs b/src/EggEncoder/Results/ProbeResult.cs index de5d580..3035bc0 100644 --- a/src/EggEncoder/Results/ProbeResult.cs +++ b/src/EggEncoder/Results/ProbeResult.cs @@ -36,6 +36,6 @@ public class ProbeResult public int? Height { get; init; } - public string? WaveformResult { get; set; } + public IReadOnlyList? Waveform { get; init; } } }