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 @@ -43,7 +43,7 @@ dotnet test src/EggEncoder.UnitTests/EggEncoder.UnitTests.csproj --configuration
- **`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.Waveform`
- **`Results/ProbeResult.cs`** — the public `ProbeResult` DTO returned by every `Probe` call
- **`ServiceCollectionExtensions.cs`** — `AddEggEncoder()` DI registration; no options, registers `IMediaEncoder` → `NativeEncoder` (scoped)
- **`ServiceCollectionExtensions.cs`** — `AddEggEncoder(enableLogging: true)` DI registration; registers `IMediaEncoder` → `NativeEncoder` (scoped). `enableLogging: false` fully silences `NativeEncoder`'s start/completion/failure logs

### Native binary packaging

Expand Down
10 changes: 6 additions & 4 deletions docs/Configuration.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>Configuration — EggEncoder</title>
<meta name="description" content="EggEncoder has no configuration surface — AddEggEncoder() takes no options.">
<meta name="description" content="EggEncoder has a single configuration knob — AddEggEncoder(enableLogging: false) to silence its logging.">
<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 @@ -42,10 +42,12 @@
<div class="content-wrapper">
<h1>Configuration</h1>

<h2>There Isn't Any</h2>
<p>EggEncoder is fully native and has no options to set. <code>AddEggEncoder()</code> takes no parameters:</p>
<pre><code class="language-csharp">builder.Services.AddEggEncoder();</code></pre>
<h2>Almost None</h2>
<p>EggEncoder is fully native and has one optional setting: whether it logs. <code>AddEggEncoder()</code> takes an optional <code>enableLogging</code> parameter (default <code>true</code>):</p>
<pre><code class="language-csharp">builder.Services.AddEggEncoder(); // logs via ILogger&lt;NativeEncoder&gt; (default)
builder.Services.AddEggEncoder(enableLogging: false); // fully silent</code></pre>
<p>This registers <code>IMediaEncoder</code> &rarr; <code>NativeEncoder</code> (scoped). There's no engine to pick, no binary path to provide, and nothing that can fail to resolve at startup due to missing external tooling.</p>
<p>Every <code>Probe</code>/<code>ConvertFile</code>/<code>CutFile</code> call logs a start and a completion message at <code>LogInformation</code>, and logs (then rethrows) any failure at <code>LogError</code> — set <code>enableLogging: false</code> to suppress all of it, or leave it on and control verbosity the standard way via your app's logging configuration (e.g. <code>"Logging:LogLevel:EggEncoder.NativeEncoder": "None"</code> in <code>appsettings.json</code>).</p>

<h2>Native Binary Resolution</h2>
<p>The bundled <code>libmp3lame.dll</code> and <code>libFLAC.dll</code> are located automatically at runtime by <code>NativeLibraryLoader</code>, relative to <code>AppContext.BaseDirectory</code> — this is not configurable and requires no setup. See <a href="Advanced-Features.html#native-binary-packaging">Native Binary Packaging</a> for how the files get there.</p>
Expand Down
2 changes: 1 addition & 1 deletion docs/Dependency-Injection.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<h1>Dependency Injection</h1>

<h2>What AddEggEncoder Registers</h2>
<p><code>services.AddEggEncoder()</code> registers exactly one service: <code>IMediaEncoder</code> &rarr; <code>NativeEncoder</code> (scoped). Inject it into a controller, a scoped service, or resolve it from an <code>IServiceScope</code> in a background worker.</p>
<p><code>services.AddEggEncoder()</code> registers exactly one service: <code>IMediaEncoder</code> &rarr; <code>NativeEncoder</code> (scoped). Inject it into a controller, a scoped service, or resolve it from an <code>IServiceScope</code> in a background worker. Pass <code>AddEggEncoder(enableLogging: false)</code> to silence <code>NativeEncoder</code>'s logging entirely — see <a href="Configuration.html">Configuration</a>.</p>

<h2>Typical Registration</h2>
<pre><code class="language-csharp">// Program.cs
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ <h2>Key Features</h2>
<div class="feature-card">
<div class="feature-icon">&#128268;</div>
<div class="feature-title">One-Line DI</div>
<p class="feature-desc"><code>services.AddEggEncoder()</code> registers <code>IMediaEncoder</code> — no options, no config required.</p>
<p class="feature-desc"><code>services.AddEggEncoder()</code> registers <code>IMediaEncoder</code> — no config required, one optional flag to silence logging.</p>
</div>
</div>

Expand Down
3 changes: 2 additions & 1 deletion llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Only fields this library can genuinely compute are populated -- nothing is fille
```csharp
using EggEncoder;

builder.Services.AddEggEncoder(); // no options — registers IMediaEncoder -> NativeEncoder (scoped)
builder.Services.AddEggEncoder(); // registers IMediaEncoder -> NativeEncoder (scoped)
// optional: AddEggEncoder(enableLogging: false) to silence Start/Completed/Failed logging entirely
```

### Consuming IMediaEncoder
Expand Down
69 changes: 69 additions & 0 deletions src/EggEncoder.UnitTests/NativeEncoderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,75 @@ public async Task CutFile_ToNestedMissingDirectory_Should_CreateDirectory_And_Cu
}
}

[Fact]
public async Task Probe_Should_Log_Start_And_Completion()
{
await _nativeEncoder.Probe(_wavFixturePath);

_logger.Invocations.Should().HaveCountGreaterThanOrEqualTo(2, "both a start and a completion message should be logged");
}

[Fact]
public async Task ConvertFile_Should_Log_Start_And_Completion()
{
var tempDirectory = CreateTempDirectory();

try
{
var destPath = Path.Combine(tempDirectory, "dest.flac");

await _nativeEncoder.ConvertFile(_wavFixturePath, destPath);

_logger.Invocations.Should().HaveCountGreaterThanOrEqualTo(2, "both a start and a completion message should be logged");
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}

[Fact]
public async Task CutFile_Should_Log_Start_And_Completion()
{
var tempDirectory = CreateTempDirectory();

try
{
var destPath = Path.Combine(tempDirectory, "cut.wav");

await _nativeEncoder.CutFile(_wavFixturePath, destPath, 0, 1);

_logger.Invocations.Should().HaveCountGreaterThanOrEqualTo(2, "both a start and a completion message should be logged");
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}

[Fact]
public async Task Probe_WithLoggingDisabled_Should_Not_Log()
{
var logger = new Mock<ILogger<NativeEncoder>>();
var encoder = new NativeEncoder(logger.Object, enableLogging: false);

await encoder.Probe(_wavFixturePath);

logger.Invocations.Should().BeEmpty();
}

[Fact]
public async Task Probe_WithLoggingDisabled_OnFailure_Should_Not_Log()
{
var logger = new Mock<ILogger<NativeEncoder>>();
var encoder = new NativeEncoder(logger.Object, enableLogging: false);

var act = () => encoder.Probe("file.ogg");

await act.Should().ThrowExactlyAsync<NotSupportedException>();
logger.Invocations.Should().BeEmpty();
}

private static void AssertNonEmptyWaveform(IReadOnlyList<double>? waveform)
{
waveform.Should().NotBeNull();
Expand Down
42 changes: 33 additions & 9 deletions src/EggEncoder/NativeEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ public class NativeEncoder : IMediaEncoder
private const int FramesPerBlock = 4096;

private readonly ILogger _logger;
private readonly bool _loggingEnabled;

public NativeEncoder(ILogger<NativeEncoder> logger)
public NativeEncoder(ILogger<NativeEncoder> logger, bool enableLogging = true)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_loggingEnabled = enableLogging;
}

public Task<ProbeResult> Probe(string filePath)
{
try
{
_logger.LogInformation($"Start native probe '{filePath}'");
LogInformation($"Start native probe '{filePath}'");

var extension = Path.GetExtension(filePath).ToLowerInvariant();
var result = extension switch
Expand All @@ -39,13 +41,13 @@ public Task<ProbeResult> Probe(string filePath)
_ => throw new NotSupportedException($"Probing '{extension}' files is not supported by the native audio encoder")
};

_logger.LogInformation($"Probe '{filePath}', format: '{result.FormatName}', codec: '{result.CodecName}', duration: {result.DurationSeconds}s");
LogInformation($"Completed native probe '{filePath}': format '{result.FormatName}', codec '{result.CodecName}', duration {result.DurationSeconds}s");

return Task.FromResult(result);
}
catch (Exception e)
{
_logger.LogError(e, $"Failed to probe '{filePath}' {e.Message}");
LogError(e, $"Failed to probe '{filePath}' {e.Message}");
throw;
}
}
Expand All @@ -54,16 +56,18 @@ public Task ConvertFile(string sourceFilePath, string destFilePath)
{
try
{
_logger.LogInformation($"Start native convert '{sourceFilePath}' to '{destFilePath}'");
LogInformation($"Start native convert '{sourceFilePath}' to '{destFilePath}'");

EnsureDestinationDirectory(destFilePath);
AudioCutter.Convert(sourceFilePath, destFilePath);

LogInformation($"Completed native convert '{sourceFilePath}' to '{destFilePath}'");

return Task.CompletedTask;
}
catch (Exception e)
{
_logger.LogError(e, $"Failed to convert from '{sourceFilePath}' to '{destFilePath}' {e.Message}");
LogError(e, $"Failed to convert from '{sourceFilePath}' to '{destFilePath}' {e.Message}");
throw;
}
}
Expand All @@ -72,20 +76,40 @@ public Task CutFile(string sourceFilePath, string destFilePath, int startInSecon
{
try
{
_logger.LogInformation($"Start native cut '{sourceFilePath}' to '{destFilePath}' start {startInSeconds} end {endInSeconds}");
LogInformation($"Start native cut '{sourceFilePath}' to '{destFilePath}' start {startInSeconds} end {endInSeconds}");

EnsureDestinationDirectory(destFilePath);
AudioCutter.Cut(sourceFilePath, destFilePath, startInSeconds, endInSeconds);
var produced = AudioCutter.Cut(sourceFilePath, destFilePath, startInSeconds, endInSeconds);

LogInformation(produced
? $"Completed native cut '{sourceFilePath}' to '{destFilePath}'"
: $"Completed native cut '{sourceFilePath}' to '{destFilePath}': requested range was outside the source duration, no file written");

return Task.CompletedTask;
}
catch (Exception e)
{
_logger.LogError(e, $"Failed to cut from '{sourceFilePath}' to '{destFilePath}' {e.Message}");
LogError(e, $"Failed to cut from '{sourceFilePath}' to '{destFilePath}' {e.Message}");
throw;
}
}

private void LogInformation(string message)
{
if (_loggingEnabled)
{
_logger.LogInformation(message);
}
}

private void LogError(Exception exception, string message)
{
if (_loggingEnabled)
{
_logger.LogError(exception, message);
}
}

private static void EnsureDestinationDirectory(string destFilePath)
{
var directory = Path.GetDirectoryName(destFilePath);
Expand Down
6 changes: 4 additions & 2 deletions src/EggEncoder/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace EggEncoder
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddEggEncoder(this IServiceCollection services)
public static IServiceCollection AddEggEncoder(this IServiceCollection services, bool enableLogging = true)
{
services.AddScoped<IMediaEncoder, NativeEncoder>();
services.AddScoped<IMediaEncoder>(serviceProvider =>
new NativeEncoder(serviceProvider.GetRequiredService<ILogger<NativeEncoder>>(), enableLogging));

return services;
}
Expand Down
Loading