AacEncoder turns float samples your code holds (from a synthesizer, a game, a microphone or a decoder) into AAC-LC packets, and Mp4AudioWriter puts them in an M4A file. With Adts = true each packet carries its own header instead, so the packets can go straight onto a socket or into a .aac file. The samples here come from Wikipedia’s example Ogg file (music by JesseW, CC BY-SA 3.0); to convert a file to M4A in one call, see Convert WAV and MP3 to M4A.
dotnet new console -o EncodeAac && cd EncodeAac dotnet add package RecursiveAI.OneAudio --version 1.7.1 curl -L -o song.ogg https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg
Put this in Program.cs and run dotnet run:
using OneAudio; using OneAudio.Aac; using OneAudio.Mp4; // Interleaved float samples, full scale ±1. Any source will do. AudioClip clip = Audio.Load("song.ogg"); int rate = clip.Format.SampleRate, channels = clip.Format.Channels; Console.WriteLine($"input: {clip.Frames} frames, {rate} Hz, {channels} ch"); // One M4A file per rate control mode, all at 96 kbit/s. AacRateControl[] modes = [AacRateControl.Cbr, AacRateControl.Abr, AacRateControl.Vbr]; foreach (AacRateControl mode in modes) { var settings = new AacEncoderSettings { Bitrate = 96_000, RateControl = mode, // Vbr uses VbrQuality, 0.5 by default }; var encoder = new AacEncoder(rate, channels, settings); string path = mode.ToString().ToLowerInvariant() + ".m4a"; long audioBytes; using (FileStream file = File.Create(path)) using (var mp4 = Mp4AudioWriter.CreateAac(file, rate, channels, encoder.AudioSpecificConfig, AacEncoder.PrimingFrames)) { mp4.FastStart = true; // moov before the audio, for progressive playback audioBytes = Encode(encoder, clip.Samples.Span, p => mp4.AddSample(p.Data.Span, p.Duration)); mp4.Finish(encoder.FramesWritten); // the edit list trims priming and padding } Report(path, encoder, audioBytes); } // ADTS headers instead: each packet stands alone, for streaming. var adts = new AacEncoderSettings { Bitrate = 96_000, Adts = true }; var adtsEncoder = new AacEncoder(rate, channels, adts); using (FileStream stream = File.Create("cbr.aac")) // or a socket, a pipe... { long bytes = Encode(adtsEncoder, clip.Samples.Span, p => stream.Write(p.Data.Span)); stream.Close(); Report("cbr.aac", adtsEncoder, bytes); } // Write takes whole 1024-frame blocks and returns how many frames it took. static long Encode(AacEncoder enc, ReadOnlySpan<float> pcm, Action<AacPacket> output) { long bytes = 0; while (pcm.Length > 0) { int frames = enc.Write(pcm); pcm = pcm[(frames * enc.Channels)..]; while (enc.TryReadPacket(out AacPacket packet)) { bytes += packet.Data.Length; output(packet); } } enc.Finish(); // pads the last block and flushes while (enc.TryReadPacket(out AacPacket packet)) { bytes += packet.Data.Length; output(packet); } return bytes; } // The audio's bit rate: packet bytes over the time the packets cover. static void Report(string path, AacEncoder enc, long audioBytes) { double seconds = enc.UnitsProduced * 1024.0 / enc.SampleRate; using IAudioDecoder d = Audio.Open(path); Console.WriteLine($"{path}: {enc.UnitsProduced} packets, " + $"{new FileInfo(path).Length:N0} bytes, " + $"{audioBytes * 8 / seconds / 1000:F1} kbit/s"); Console.WriteLine($" low-pass {enc.Bandwidth} Hz, " + $"decodes to {d.LengthFrames} frames"); }
input: 269188 frames, 44100 Hz, 2 ch cbr.m4a: 264 packets, 75,338 bytes, 95.8 kbit/s low-pass 16500 Hz, decodes to 269188 frames abr.m4a: 264 packets, 74,802 bytes, 95.1 kbit/s low-pass 16500 Hz, decodes to 269188 frames vbr.m4a: 264 packets, 76,292 bytes, 97.0 kbit/s low-pass 16500 Hz, decodes to 269188 frames cbr.aac: 264 packets, 75,236 bytes, 98.2 kbit/s low-pass 16500 Hz, decodes to 270336 frames
Writereturns the frames it took. It fills one block of 1,024 frames at a time, and a packet comes out once the block after it is in, because the encoder looks one block ahead to choose its windows.Finishpads the last block with silence and flushes the rest.- A packet’s
Datais valid until the next call on the encoder: copy it or write it out before encoding more. After construction,WriteandTryReadPacketallocate nothing. - Decoded AAC starts with 1,024 frames of priming (
PrimingFrames) and ends withEndPaddingFramesof padding.Mp4AudioWriter.Finish(FramesWritten)records both in an edit list and an iTunesiTunSMPBtag, so the M4A decodes to exactly the input’s length. ADTS has nowhere to record them, socbr.aacdecodes 1,024 frames longer plus the padding. FastStart = true, set beforeFinish, puts themoovbox in front of the audio so a player can start before the download ends.Finishthen moves the audio forward inside the file, so the stream must be readable as well as seekable (File.Createis). It is off by default onMp4AudioWriter, and on by default forAudio.Transcode(TranscodeOptions.FastStart).
Bit rate modes
Cbr(the default): every block is sized fromBitrate, with a bit reservoir for blocks that need more, so the stream averages the target. Suits streaming and fixed bandwidth.Abr: block sizes follow how hard the music is to code, steered by a reservoir of about eight blocks, so they vary much more than withCbrwhile the file still averagesBitrate.Vbr: bits follow how hard each block is to code, scaled byVbrQuality(0 to 1, default 0.5; each 0.5 doubles the bits).Bitratesets the bandwidth and the range, and the average depends on the music.Bitrateis for all channels together; the default is 64,000 per channel. The low-pass (Bandwidth) follows the rate per channel unless you set it: 16.5 kHz at 96 kbit/s stereo, as above.
cbr.m4a and abr.m4a above are different files. FFmpeg 8.0.1’s ffprobe reads the same audio bit rates the program printed, 95.8 kbit/s for CBR and 95.1 for ABR, both within 1% of the 96 kbit/s target. The project’s changelog says only that an ABR file “still averages the target within 5 %”.
for f in cbr.m4a abr.m4a vbr.m4a; do ffprobe -v error -of compact=p=0 -show_entries format=filename,size:stream=bit_rate $f done
bit_rate=95774 filename=cbr.m4a|size=75338 bit_rate=95074 filename=abr.m4a|size=74802 bit_rate=97019 filename=vbr.m4a|size=76292
The top-level boxes of each M4A, with moov ahead of the audio in mdat:
ffprobe -v trace cbr.m4a 2>&1 | grep -o "type:'....' parent:'root'"type:'ftyp' parent:'root' type:'moov' parent:'root' type:'mdat' parent:'root'
Noise substitution and other decoders
NoiseSubstitution is on by default: bands above 4 kHz that would otherwise decode as silence are coded as noise of the right energy, and each decoder generates that noise itself. In the project’s README, “this library’s decode and ffmpeg’s agree to about 1e-7 outside noise-substituted bands, while those bands match in energy but not sample by sample (noise generation is decoder-specific; NoiseSubstitution = false makes every band deterministic)”. This program encodes the song both ways and compares oneaudio’s decode with FFmpeg 8.0.1’s, which it runs as ffmpeg -v error -i <file> -f f32le -:
using System.Diagnostics; using System.Runtime.InteropServices; using OneAudio; using OneAudio.Aac; foreach (bool pns in new[] { true, false }) { string path = pns ? "pns-on.m4a" : "pns-off.m4a"; var aac = new AacEncoderSettings { Bitrate = 96_000, NoiseSubstitution = pns }; Audio.Transcode("song.ogg", path, new TranscodeOptions { Aac = aac }); float[] ours = Audio.Load(path).Samples.ToArray(); float[] theirs = FFmpegDecode(path); int n = Math.Min(ours.Length, theirs.Length); // ffmpeg keeps the end padding double max = 0, signal = 0, error = 0; for (int i = 0; i < n; i++) { double d = ours[i] - theirs[i]; max = Math.Max(max, Math.Abs(d)); signal += (double)ours[i] * ours[i]; error += d * d; } Console.WriteLine($"{path}: largest difference {max:E1}, " + $"SNR {10 * Math.Log10(signal / error):F1} dB"); } // Interleaved 32-bit floats from ffmpeg's own AAC decoder. static float[] FFmpegDecode(string path) { var start = new ProcessStartInfo("ffmpeg", $"-v error -i {path} -f f32le -") { RedirectStandardOutput = true, }; using Process ffmpeg = Process.Start(start)!; using var pcm = new MemoryStream(); ffmpeg.StandardOutput.BaseStream.CopyTo(pcm); ffmpeg.WaitForExit(); return MemoryMarshal.Cast<byte, float>(pcm.ToArray()).ToArray(); }
pns-on.m4a: largest difference 8.4E-004, SNR 41.2 dB pns-off.m4a: largest difference 4.5E-008, SNR 136.0 dB
With noise substitution off, the two decodes differ by rounding only. With it on, the difference is the two decoders’ own noise in the substituted bands. FFmpeg’s decode runs 124 frames longer because it keeps the padding at the end, so the comparison covers the frames both have.
Check the files
FFmpeg 8.0.1’s ffprobe reads every file as AAC, LC profile, 44,100 Hz, 2 channels: the M4A files at 6.104036 s, the input’s length, and cbr.aac at about 6.12 s, estimated from the bit rate, since raw ADTS has no index. ffmpeg -v warning -i <file> -f null - decodes them all with no error; for cbr.aac it only notes that the duration is estimated. Headless Chrome 149 played cbr.m4a, abr.m4a, vbr.m4a and cbr.aac in an <audio> element, readyState 4 with currentTime advancing.
Limits
- Mono or stereo, at 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100 or 48000 Hz. Other values throw
ArgumentExceptionfrom the constructor; resample first withAudio.ResampleorLoadOptions.SampleRate. - AAC-LC only: no HE-AAC, TNS, intensity stereo or LTP.
Mp4AudioWriterneeds a seekable stream (readable too forFastStart) and writes one audio track. For live output, use ADTS.- One encoder per stream, not thread-safe.
The whole API, including FlacPacketEncoder and FLAC in MP4, is in the API overview.