Recursive AI

Audio in .NET without FFmpeg

When you can drop FFmpeg, FFMpegCore or Xabe.FFmpeg for audio in .NET: ffmpeg commands mapped to managed C# calls, and the jobs that still need FFmpeg.

You can do audio in .NET without FFmpeg when your inputs are WAV, MP3, Ogg Vorbis, Opus or FLAC, your outputs are WAV, FLAC or Opus, and the work is converting, resampling, mixing, measuring loudness or normalizing. A managed library such as OneAudio does all of that in-process, with no ffmpeg binary to install, ship or keep patched, and it publishes with NativeAOT and trimming. You still need FFmpeg for AAC and M4A, for MP3 or Vorbis encoding, for video, and for the long tail of other formats. This page maps the common ffmpeg audio commands to C# calls, with every sample compiled and run against OneAudio 1.2.1 and FFMpegCore 5.5.0 on .NET 10.

What an FFmpeg wrapper brings with it

The .NET FFmpeg wrappers run the ffmpeg program for you. FFMpegCore 5.5.0 (MIT) is a “FFMpeg/FFProbe wrapper”, and its README’s “Binaries” section has you install ffmpeg and ffprobe through a package manager, download them, or fetch them at run time with the FFMpegCore.Extensions.Downloader package; by default it expects to find them on the PATH. Converting a file with it, on a machine with ffmpeg installed from the distribution’s packages:

Program.csFFMpegCore 5.5.0 · Linux
using FFMpegCore;
 
// FFMpegCore runs the ffmpeg and ffprobe programs; here they come from PATH (apt install ffmpeg).
FFMpegArguments
    .FromFileInput("music.mp3")
    .OutputToFile("music-ffmpeg.wav", true)
    .ProcessSynchronously();
 
IMediaAnalysis info = FFProbe.Analyse("music-ffmpeg.wav");
Console.WriteLine($"{info.PrimaryAudioStream?.SampleRateHz} Hz, {info.PrimaryAudioStream?.Channels} ch, {info.Duration}");
OutputConsole
44100 Hz, 2 ch, 00:00:30

That works, and for video or unusual formats it is the right tool. The costs are around it: every server, container and desktop install needs an ffmpeg build of the right architecture, the codecs you get depend on how that build was configured, each conversion starts a separate process, and errors come back as process output. Xabe.FFmpeg 6.1.1 wraps the same program; its license page (read October 27, 2026) offers it under CC BY-NC-SA 3.0 for non-commercial projects and points to paid plans on its pricing page otherwise. Read the license text for your own case.

The same jobs in managed C#

TerminalNuGet
dotnet new console -o NoFfmpeg
cd NoFfmpeg
dotnet add package RecursiveAI.OneAudio --version 1.2.1
Program.csOneAudio 1.2.1 · .NET 10
using OneAudio;
using OneAudio.Loudness;
using OneAudio.Opus;
 
// ffmpeg -i music.mp3 music-from-mp3.wav
Audio.Transcode("music.mp3", "music-from-mp3.wav");
 
// ffmpeg -i music.mp3 -ar 16000 -ac 1 speech.wav
Audio.Transcode("music.mp3", "speech.wav", new TranscodeOptions { SampleRate = 16000, Channels = 1 });
 
// ffmpeg -i music.wav -c:a flac music.flac      and      ffmpeg -i music.wav -c:a libopus -b:a 128k music.opus
Audio.Transcode("music.wav", "music.flac");
Audio.Transcode("music.wav", "music.opus", new TranscodeOptions
{
    Opus = new OpusFileWriterSettings { Encoder = new OpusEncoderSettings { BitrateBps = 128_000 } },
});
 
// ffmpeg -i music.wav -af ebur128=peak=true -f null -
LoudnessResult r = Audio.Measure("music.wav");
Console.WriteLine($"{r.IntegratedLufs:F1} LUFS, {r.LoudnessRange:F1} LU, {r.TruePeakDbtp:F1} dBTP");
 
// Gain to -14 LUFS with a -1 dBTP ceiling, one linear gain for the whole file.
NormalizeResult n = Audio.Normalize("music.wav", "music-14.wav");
Console.WriteLine($"gain {n.GainDb:+0.0;-0.0} dB, limited by peak: {n.LimitedByPeak}");
 
foreach (string f in new[] { "music-from-mp3.wav", "speech.wav", "music.flac", "music.opus", "music-14.wav" })
{
    using IAudioDecoder d = Audio.Open(f);
    Console.WriteLine($"{f}: {d.Format.SampleRate} Hz, {d.Format.Channels} ch, {d.LengthFrames / (double)d.Format.SampleRate:F3} s");
}
OutputConsole
-14.3 LUFS, 2.8 LU, -5.1 dBTP
gain +0.3 dB, limited by peak: False
music-from-mp3.wav: 44100 Hz, 2 ch, 30.000 s
speech.wav: 16000 Hz, 1 ch, 30.000 s
music.flac: 44100 Hz, 2 ch, 30.000 s
music.opus: 48000 Hz, 2 ch, 30.000 s
music-14.wav: 44100 Hz, 2 ch, 30.000 s
ffmpegOneAudio 1.2.1
ffmpeg -i in.mp3 out.wavAudio.Transcode("in.mp3", "out.wav")
-ar 48000, -ac 1TranscodeOptions { SampleRate = 48000, Channels = 1 }
-c:a pcm_s24leWav = new WavEncoderSettings { SampleType = NativeSampleType.PcmSigned24 }
-c:a flac -compression_level 8Flac = new FlacEncoderSettings { CompressionLevel = 8 }
-c:a libopus -b:a 128kOpus = new OpusFileWriterSettings { Encoder = new OpusEncoderSettings { BitrateBps = 128_000 } }
-af ebur128=peak=trueAudio.Measure(path): integrated LUFS, loudness range, true peak
-af loudnorm (linear, two passes)Audio.Normalize(input, output): −14 LUFS and −1 dBTP by default
-af aresampleAudio.Resample, or SampleRate in the options
-filter_complex amixAudioMixer: voices, buses, ducking, effects
Pipes: -i pipe:0 -f wav pipe:1Audio.Transcode(inputStream, outputStream, options) with Format set
  • The output format comes from the extension, or from TranscodeOptions.Format for streams. Conversion streams block by block, so memory doesn’t grow with the file, and a failed file conversion deletes the partial output.
  • The loudness figures match ffmpeg’s: its ebur128 filter with peak=true reports −14.3 LUFS integrated, 2.8 LU of loudness range and a −5.1 dBFS true peak for the same file.
  • Audio.Normalize applies one gain to the whole file and holds it back if the true peak would pass the ceiling. FFmpeg’s loudnorm documentation describes “both dynamic and linear normalization modes”; the linear one is the equivalent. There is no dynamic mode in OneAudio. More in loudness normalization in C#.
  • A lossless source written as WAV or FLAC without conversion is bit-exact, and MP3 decoding trims the encoder’s padding, so lengths match the original (30.000 s above).

What you gain by dropping the binary

  • Deployment: one NuGet package of managed code. dotnet publish for linux-arm64, a Windows installer or a slim container image needs nothing else, and a NativeAOT app with every decoder is 1.96 MB on linux-x64, per the project’s README.
  • Errors as exceptions: malformed input raises AudioFormatException, an unsupported format UnsupportedAudioFormatException, and a file that exceeds a configured limit AudioLimitExceededException, in your process, with no output to parse.
  • Untrusted uploads: sizes read from a file are checked against AudioLimits before anything is allocated, and each of the six parsers was fuzzed with libFuzzer for 660 seconds with no crashes, hangs or out-of-memory failures, per the README’s fuzzing table.
  • Samples in your code: decode to a float span, process it, and encode it, without pipes or temporary files. The same package plays and records through its Devices companion (play audio in C#).

When you still need FFmpeg

  • AAC, M4A, ALAC, WMA and other formats outside WAV, MP3, Ogg (Vorbis, Opus, FLAC) and FLAC.
  • MP3 or Vorbis output. OneAudio encodes WAV, FLAC and Opus. If a destination requires MP3 or AAC, keep FFmpeg for that step.
  • Video: extracting audio from MP4 or MKV, muxing audio into video, or anything with frames.
  • Dynamic loudness processing such as loudnorm’s dynamic mode, compressors and other ffmpeg filters beyond the mixer’s filters, reverb and limiter.

You can also combine them: handle WAV, MP3, Ogg and FLAC in process, and call FFmpeg only for the formats that need it. Then only the services that see those formats need an ffmpeg binary.

Where next