---
title: "Encode AAC in C#: samples to M4A or ADTS"
url: https://recursiveai.org/oneaudio/cookbook/encode-aac/
description: "Encode float PCM samples to AAC in C# with AacEncoder: write a fast-start M4A with Mp4AudioWriter, or ADTS packets for streaming, at constant, average or variable bit rate."
version: "1.7.1"
---

# Encode AAC in C#: samples to M4A or ADTS

`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](https://commons.wikimedia.org/wiki/File:Example.ogg)); to convert a file to M4A in one call, see [Convert WAV and MP3 to M4A](https://recursiveai.org/oneaudio/cookbook/convert-wav-to-m4a/).

```shell
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`:

```csharp
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");
}
```

```text
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
```

- `Write` returns 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. `Finish` pads the last block with silence and flushes the rest.
- A packet’s `Data` is valid until the next call on the encoder: copy it or write it out before encoding more. After construction, `Write` and `TryReadPacket` allocate nothing.
- Decoded AAC starts with 1,024 frames of priming (`PrimingFrames`) and ends with `EndPaddingFrames` of padding. `Mp4AudioWriter.Finish(FramesWritten)` records both in an edit list and an iTunes `iTunSMPB` tag, so the M4A decodes to exactly the input’s length. ADTS has nowhere to record them, so `cbr.aac` decodes 1,024 frames longer plus the padding.
- `FastStart = true`, set before `Finish`, puts the `moov` box in front of the audio so a player can start before the download ends. `Finish` then moves the audio forward inside the file, so the stream must be readable as well as seekable (`File.Create` is). It is off by default on `Mp4AudioWriter`, and on by default for `Audio.Transcode` (`TranscodeOptions.FastStart`).

## Bit rate modes

- `Cbr` (the default): every block is sized from `Bitrate`, 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 with `Cbr` while the file still averages `Bitrate`.
- `Vbr`: bits follow how hard each block is to code, scaled by `VbrQuality` (0 to 1, default 0.5; each 0.5 doubles the bits). `Bitrate` sets the bandwidth and the range, and the average depends on the music.
- `Bitrate` is 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 %”.

```shell
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
```

```text
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`:

```shell
ffprobe -v trace cbr.m4a 2>&1 | grep -o "type:'....' parent:'root'"
```

```text
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 -`:

```csharp
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();
}
```

```text
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 `ArgumentException` from the constructor; resample first with `Audio.Resample` or `LoadOptions.SampleRate`.
- AAC-LC only: no HE-AAC, TNS, intensity stereo or LTP.
- `Mp4AudioWriter` needs a seekable stream (readable too for `FastStart`) 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](https://recursiveai.org/oneaudio/docs/api/#aac-lc-encoding-flac-packets-and-the-mp4-writer).
