aac
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 channelaac 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.
| Function | Does |
|---|---|
parse_config(asc) -> Config | An AudioSpecificConfig (an MP4 track’s config) |
parse_adts(data) -> Adts | The ADTS header at the start of data: config, header_len, frame_len, blocks |
decoder() -> Decoder | A 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) -> i64 | One raw block (an MP4 sample) into out; samples a channel |
d.decode_adts(frame, out) -> i64 | One 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
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.
| Function | Does |
|---|---|
encoder(rate, channels, bitrate) -> Encoder | An encoder, or Unsupported |
e.config() -> []const u8 | The AudioSpecificConfig (2 bytes) an MP4’s track carries |
e.encode(pcm) -> ?Vec_u8 | 1024 frames a channel of f32 samples (±1.0, interleaved): one block, or null the first time |
e.encode_i16(pcm) -> ?Vec_u8 | The same from 16-bit samples (what audio.Recorder.read_i16 gives) |
e.flush() -> ?Vec_u8 | After the last input: the blocks still held (two), then null |
e.priming() -> i64 | 1024: decoded samples before the input’s first |
e.length() -> i64 | The 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: