aac

tape
import aac from "aac";

// an ADTS stream (.aac): each frame's header configures the decoder
var d: aac.Decoder = aac.decoder();
defer d.free();
var out: [8192]f32;
var pos: i64 = 0;
while (pos < data.len) {
    let h: aac.Adts = aac.parse_adts(data[pos..]) or { return 1; };
    let n: i64 = d.decode_adts(data[pos..pos + h.frame_len], out[0..]) or {
        io.println("cannot decode it: {d.last_error()}");
        return 1;
    };
    play(out[0..n * h.config.channels], h.config.rate, h.config.channels);
    pos = pos + h.frame_len;
}

// an MP4's AAC track: configure once, then each sample is a block
let c: aac.Config = aac.parse_config(track.config) or { return 1; };
d.configure(c) or { return 1; };
let n: i64 = d.decode(sample_bytes, out[0..]) or { return 1; };   // 1024 a channel

aac decodes AAC-LC, the AAC almost every MP4, .m4a and ADTS stream carries, to f32 samples at full scale ±1.0, interleaved, 1024 a channel per block. It is pure and sans-I/O: bytes in, samples out. Mono and stereo, 7350 Hz to 96 kHz, with every tool of the profile (M/S and intensity stereo, PNS, TNS, pulses, sine and KBD windows). An HE-AAC stream decodes to its AAC-LC core at half its rate; Config.sbr tells.

FunctionDoes
parse_config(asc) -> ConfigAn AudioSpecificConfig (an MP4 track’s config)
parse_adts(data) -> AdtsThe ADTS header at the start of data: config, header_len, frame_len, blocks
decoder() -> DecoderA decoder; configure it, or let decode_adts do it
d.configure(config)Decode with this configuration (a new rate or channel count starts afresh)
d.decode(block, out) -> i64One raw block (an MP4 sample) into out; samples a channel
d.decode_adts(frame, out) -> i64One ADTS frame, header included
d.reset()Forget the overlap with the last block: after a seek
d.last_error(), d.free()What went wrong; give the memory back

A Config has object_type (2, or 5 with SBR), rate, rate_index, channels and sbr. out must hold 1024 samples per channel per block (OutTooSmall otherwise). The first block overlaps silence: an encoder’s priming samples come out first, and an MP4’s edit list says how many to drop.

What it refuses is Unsupported with the reason: more than two channels, coupling channels, the 960 sample frame, AAC Main and SSR. Damaged frames give Corrupt, never a crash; decoding carries on with the next frame.

Encoding

tape
import aac from "aac";
import mp4 from "mp4";
import collections from "collections";

// a voice note: mono, 48 kHz, 48 kbit/s
var enc: aac.Encoder = aac.encoder(48000, 1, 48000) or { return 1; };
defer enc.free();
var w: mp4.Writer = mp4.writer_aac(48000, 1, enc.config());
defer w.free();
// each 1024 frames of input give a block (null the first time)
let b: ?collections.Vec_u8 = enc.encode_i16(samples);      // or encode(f32 samples)
if (b != null) { w.add(b!.slice()); }
// after the last input, what the encoder still holds
while (true) {
    let t: ?collections.Vec_u8 = enc.flush();
    if (t == null) { break; }
    w.add(t!.slice());
}
// the .m4a: the edit list cuts the encoder's priming and the padding
let file: collections.Vec_u8 = w.finish(enc.priming(), enc.length());

aac.encoder(rate, channels, bitrate) makes an AAC-LC encoder: mono or stereo, at 16, 22.05, 24, 32, 44.1 or 48 kHz, from 16 kbit/s up to 128 kbit/s a channel (and at most 4 × the rate a channel: 64 kbit/s at 16 kHz). Anything else is Unsupported. The stream it writes is what the phones record and play; Windows’s Media Foundation, ffmpeg and tape’s decoder all decode it to the same samples.

FunctionDoes
encoder(rate, channels, bitrate) -> EncoderAn encoder, or Unsupported
e.config() -> []const u8The AudioSpecificConfig (2 bytes) an MP4’s track carries
e.encode(pcm) -> ?Vec_u81024 frames a channel of f32 samples (±1.0, interleaved): one block, or null the first time
e.encode_i16(pcm) -> ?Vec_u8The same from 16-bit samples (what audio.Recorder.read_i16 gives)
e.flush() -> ?Vec_u8After the last input: the blocks still held (two), then null
e.priming() -> i641024: decoded samples before the input’s first
e.length() -> i64The input’s frames a channel so far
e.adts_header(block_len, out)The 7-byte ADTS header that goes before a block in an .aac file
e.free()Give its memory back

Give encode exactly 1024 frames each time; only the last input may be shorter (the rest counts as silence). More than 1024 frames is a programming error and traps. A block is one MP4 sample (mp4.Writer.add); the blocks are yours (each a Vec_u8). The encoder’s output starts 1024 samples late (its priming) and ends padded to a block: an .m4a’s edit list (finish(priming, length)) tells players to cut both, so the file plays exactly the input. An ADTS file has nowhere to say it.

The encoder switches to short windows on sudden sounds (no pre-echo before a consonant or a click), shapes its noise with a psychoacoustic model, codes stereo as mid and side where that is cheaper, and keeps the bitrate with a bit reservoir: quiet passages save bits for loud ones, and the average is the bitrate asked for. The bandwidth follows the bitrate (7 kHz at 32 kbit/s a channel, 10 kHz at 48, 13 kHz at 64, up to 19 kHz). On a recorded voice (mono, 48 kHz) the decoding measured 21 dB SNR at 32 kbit/s, 28 dB at 48 and 33 dB at 64. Encoding takes some 65 µs a block natively (300 times real time for mono 48 kHz) and runs about 7 times real time in the VM.

Last modified: