Recursive AI

Decode AAC and M4A in C#

All oneaudio recipes

Audio.Load decodes AAC-LC from M4A, MP4, 3GP and raw ADTS (.aac) files to float samples, and Audio.Transcode converts them to WAV, in managed code with no Media Foundation or FFmpeg. The files here are 1.5-second synthetic test files from the oneaudio repository, one M4A and the same kind of audio as raw AAC.

Terminal
dotnet new console -o DecodeAac && cd DecodeAac
dotnet add package RecursiveAI.OneAudio --version 1.4.0
t=https://raw.githubusercontent.com/recursive-ai-oss/oneaudio-csharp/v1.4.0/testdata
curl -L -o song.m4a $t/containers/aac-itunes-gapless.m4a
curl -L -o song.aac $t/containers/aac.aac

Put this in Program.cs and run dotnet run:

C#
using OneAudio;
using OneAudio.Aac;
 
// AAC-LC in an M4A file decodes to float like any other format.
AudioClip clip = Audio.Load("song.m4a");
AudioFormat f = clip.Format;
Console.WriteLine($"song.m4a: {f.SampleRate} Hz, {f.Channels} ch, " +
                  $"{clip.Frames} frames " +
                  $"({clip.Frames / (double)f.SampleRate:F3} s)");
 
// Raw ADTS: nothing in the file records the encoder's priming and padding.
using (IAudioDecoder adts = Audio.Open("song.aac"))
    Console.WriteLine($"song.aac: {adts.GetType().Name}, {adts.LengthFrames} frames");
 
// The concrete decoder says what the stream is. Seeking is exact.
using (var aac = (AacDecoder)Audio.Open("song.m4a"))
{
    float[] block = new float[1024 * f.Channels];
    aac.Seek(44100);
    int frames = aac.Read(block);
    int at = 44100 * f.Channels, n = frames * f.Channels;
    bool same = block.AsSpan(0, n).SequenceEqual(clip.Samples.Span.Slice(at, n));
    Console.WriteLine($"object type {aac.Config.ObjectType} (AAC-LC), seek to 1 s, " +
                      $"same as a full decode: {same}");
}
 
// M4A to WAV, block by block.
TranscodeResult wav = Audio.Transcode("song.m4a", "song.wav");
Console.WriteLine($"song.wav: {wav.Frames} frames, " +
                  $"{new FileInfo("song.wav").Length:N0} bytes");
Text
song.m4a: 44100 Hz, 2 ch, 65000 frames (1.474 s)
song.aac: AacDecoder, 67584 frames
object type 2 (AAC-LC), seek to 1 s, same as a full decode: True
song.wav: 65000 frames, 260,080 bytes

If Audio.Open says “No MPEG audio frame found”

In 1.4.0, Audio checks for MP3 before MP4 and AMR, and the MP3 check can claim some M4A, 3GP and AMR files. Opening one of those throws AudioFormatException: No MPEG audio frame found. An AudioKit that checks MP4, AMR and ADTS before MP3 opens them, and has the same Open, Load, Transcode, Measure and Normalize as Audio:

C#
using OneAudio;
 
var kit = new AudioKit(new AudioCodecRegistry()
    .AddWav().AddOgg().AddVorbis().AddOpus().AddFlac()
    .AddMp4().AddAmr().AddAdts().AddMp3().AddAac());
AudioClip clip = kit.Load("song.m4a");
Console.WriteLine($"{clip.Frames} frames");
Text
65000 frames

Gapless length

An AAC encoder starts with 1,024 frames of priming and pads the end to a whole frame. An M4A file records both, in its edit list or in an iTunes iTunSMPB tag, and oneaudio trims them, so song.m4a decodes to the length that was encoded. A raw .aac file has nowhere to record them, so the whole decoded signal is kept.

Limits

  • AAC-LC, mono or stereo, 8 to 48 kHz, with every AAC-LC coding tool. A stream that carries SBR data without signalling it decodes as its AAC-LC core.
  • HE-AAC signalled in the file (SBR or PS), more than two channels, rates above 48 kHz and the other AAC object types throw UnsupportedAudioFormatException. So does ALAC in an M4A.
  • Fragmented MP4 files are rejected with a typed error. With several audio tracks, Mp4PacketSource.Tracks lists them and SelectTrack picks one.
  • Title, artist and comments come from Mp4PacketSource (OneAudio.Mp4): Mp4PacketSource.Open(File.OpenRead(path)).
  • oneaudio doesn’t write AAC. To compress, write Ogg Opus.

The decoder matches all 72 exact AAC-LC mono and stereo ISO/IEC 14496-26 conformance streams within the full-accuracy limits. Speech in AMR and 3GP files: Decode AMR.