ReviewOS

also looking at this

home-lang/home

feat(audio): add verified AAC-LC encoding and reject invalid prototype output

#918
Merged chris wants to merge feat/aac-mp4-recording into main
10 files +318 -471

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

.github/workflows/native-aac.ymladded+34-0
Changes to .github/workflows/native-aac.yml
@@ -0,0 +1,34 @@
1name: Native AAC-LC
2on:
3 push:
4 branches: [feat/aac-mp4-recording]
5 pull_request:
6 paths: ['packages/audio/src/encoding/*aac*', 'packages/video/src/codecs/audio/aac_encoder.zig', '.github/workflows/native-aac.yml']
7permissions:
8 contents: read
9jobs:
10 aac:
11 strategy:
12 fail-fast: false
13 matrix:
14 os: [ubuntu-24.04, ubuntu-24.04-arm]
15 zig: ['0.15.2', 'master']
16 runs-on: ${{ matrix.os }}
17 steps:
18 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19 - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29
20 with:
21 version: ${{ matrix.zig }}
22 - name: Build pinned standalone codec
23 run: |
24 git clone https://github.com/mstorsjo/fdk-aac.git /tmp/fdk-aac
25 git -C /tmp/fdk-aac checkout --detach 716f4394641d53f0d79c9ddac3fa93b03a49f278
26 cmake -S /tmp/fdk-aac -B /tmp/fdk-build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON
27 cmake --build /tmp/fdk-build -j 2
28 sudo cmake --install /tmp/fdk-build
29 sudo ldconfig
30 - name: Check header boundary and Zig lifecycle
31 run: |
32 cc -std=c11 -Wall -Wextra -Werror $(pkg-config --cflags fdk-aac) -c packages/audio/src/encoding/native_aac.c -o /tmp/home-aac.o
33 zig test packages/audio/src/encoding/native_aac_test.zig /tmp/home-aac.o -lc -L /usr/local/lib -lfdk-aac
34 zig test packages/audio/src/encoding/aac_encoder.zig
packages/audio/NATIVE_AAC.mdadded+41-0
Changes to packages/audio/NATIVE_AAC.md
@@ -0,0 +1,41 @@
1# Native AAC-LC encoding
2
3`src/encoding/native_aac.{c,h,zig}` is Home's opt-in streaming AAC-LC adapter.
4It links the standalone FDK AAC library directly, without FFmpeg/Libav. The C
5boundary uses the installed headers and opaque handles; no FDK layouts are
6redeclared in Zig. Home's adapter is MIT; the FDK library retains its own license.
7
8The older `AacEncoder` and `AacFullEncoder` prototypes emitted envelope bytes or
9fixed-width coefficients as if they were AAC. Their encode entry points now
10return `UnsupportedLegacyEncoder`. Configuration helpers remain available;
11use `audio.NativeAacEncoder` or `audio.encoding.native_aac.Encoder` for real AAC.
12The old prototype's partial-allocation cleanup is also fixed.
13
14Compile `native_aac.c` with the system compiler, include the installed FDK header
15root and link `fdk-aac`. Instantiate with an explicit sample rate, mono/stereo
16channels and bitrate in bits/s. Supported rates are 8, 11.025, 12, 16, 22.05, 24,
1732, 44.1 and 48kHz. Bitrate range is 16–320kb/s; unsupported combinations fail.
18No resampling, HE-AAC, implicit input device access or ADTS transport is included.
19
20`encode` accepts 1–1024 S16LE interleaved frames and returns the number consumed
21plus an optional raw access unit. Retain and feed any unconsumed input. Borrowed
22packet bytes expire at the next call. Fixed input/output buffers bound memory;
23there is no frame-sized growing history. Serialize calls on each encoder.
24
25The returned `Info` supplies the real AudioSpecificConfig, frame length and
26codec delay in samples. Keep it with the stream. Drain `flush` until `null`, then
27close. Encoding after drain fails. A codec failure poisons subsequent calls.
28The muxer must account for codec delay and final zero padding; do not timestamp
29packets from callback time or assume the codec delay is 1024/2048 samples.
30
31Validation uses FDK 2.0.3 at commit
32`716f4394641d53f0d79c9ddac3fa93b03a49f278`. Targeted tests exercise partial PCM,
33configuration bytes, delayed drain, padding coverage and state rejection on Zig
340.15.2 and Home's development toolchain. Independent camera integration decodes
35with FAAD2 and checks waveforms, segment sample counts and MP4 edit timelines.
36See `.github/workflows/native-aac.yml`; no hardware performance claim is made.
37
38```sh
39zig test packages/audio/src/encoding/native_aac_test.zig \
40 packages/audio/src/encoding/native_aac.c -lc -lfdk-aac
41```
packages/audio/src/audio.zigmodified+2-1
Changes to packages/audio/src/audio.zig
@@ -339,7 +339,8 @@ pub const encoding = ;
339339pub const Mp3Encoder = encoding.Mp3Encoder;
340340pub const Mp3Quality = encoding.Mp3Quality;
341341pub const Id3v2Writer = encoding.Id3v2Writer;
342pub const AacEncoder = encoding.AacEncoder;
342pub const AacEncoder = encoding.AacEncoder; // Legacy encode rejects; use NativeAacEncoder.
343pub const NativeAacEncoder = encoding.NativeAacEncoder;
343344pub const AacQuality = encoding.AacQuality;
344345pub const AacProfile = encoding.AacProfile;
345346pub const OpusEncoder = encoding.OpusEncoder;
packages/audio/src/encoding/aac_encoder.zigmodified+29-106
Changes to packages/audio/src/encoding/aac_encoder.zig
@@ -1,6 +1,6 @@
11// Home Audio Library - AAC Encoder
2// Pure Zig Advanced Audio Coding encoder
3// Simplified LC-AAC implementation
2// Legacy prototype/configuration helpers. encode rejects unsupported payloads.
3// For interoperable AAC-LC use native_aac.zig (opt-in standalone FDK codec).
44
55const std = ;
66const Allocator = std.mem.Allocator;
@@ -16,7 +16,7 @@ pub const AacProfile = enum(u5) {
1616 aac_scalable = 6,
1717
1818 pub fn getAudioObjectType(self: AacProfile) u5 {
19 return @intFromEnum(self);
19 return @as(u5, @bitCast(self));
2020 }
2121};
2222
@@ -198,6 +198,11 @@ pub const AacEncoder = struct {
198198 quality: AacQuality,
199199 profile: AacProfile,
200200 ) !Self {
201 const mdct_buffer = try allocator.alloc(f32, SAMPLES_PER_FRAME * 2);
202 errdefer allocator.free(mdct_buffer);
203 const prev_samples = try allocator.alloc(f32, SAMPLES_PER_FRAME * @as(usize, channels));
204 errdefer allocator.free(prev_samples);
205 const scale_factors = try allocator.alloc(u8, NUM_SCALE_FACTOR_BANDS);
201206 return Self{
202207 .allocator = allocator,
203208 .sample_rate = sample_rate,
@@ -205,9 +210,9 @@ pub const AacEncoder = struct {
205210 .bitrate = quality.getBitrate(),
206211 .profile = profile,
207212 .output = .empty,
208 .mdct_buffer = try allocator.alloc(f32, SAMPLES_PER_FRAME * 2),
209 .prev_samples = try allocator.alloc(f32, SAMPLES_PER_FRAME * @as(usize, channels)),
210 .scale_factors = try allocator.alloc(u8, NUM_SCALE_FACTOR_BANDS),
213 .mdct_buffer = mdct_buffer,
214 .prev_samples = prev_samples,
215 .scale_factors = scale_factors,
211216 .frame_count = 0,
212217 };
213218 }
@@ -219,103 +224,12 @@ pub const AacEncoder = struct {
219224 self.allocator.free(self.scale_factors);
220225 }
221226
222 /// Encode PCM samples to AAC (ADTS format)
227 /// The old envelope payload was not AAC. Never return it as encoded audio.
228 /// Use native_aac.Encoder for verified raw AAC-LC packets and codec delay.
223229 pub fn encode(self: *Self, samples: []const f32) !void {
224 const frame_samples = SAMPLES_PER_FRAME * @as(usize, self.channels);
225 var pos: usize = 0;
226
227 while (pos + frame_samples <= samples.len) {
228 try self.encodeFrame(samples[pos .. pos + frame_samples]);
229 pos += frame_samples;
230 }
231 }
232
233 /// Encode a single AAC frame
234 fn encodeFrame(self: *Self, samples: []const f32) !void {
235 // Simplified AAC encoding:
236 // 1. Apply MDCT
237 // 2. Quantize spectral coefficients
238 // 3. Huffman encode
239 // 4. Pack into ADTS frame
240
241 // For this implementation, we create simplified encoded data
242 var frame_data: std.ArrayList(u8) = .empty;
243 defer frame_data.deinit(self.allocator);
244
245 // Encode raw data block
246 try self.encodeRawDataBlock(samples, &frame_data);
247
248 // Calculate total frame size (header + data)
249 const frame_length: u13 = @intCast(7 + frame_data.items.len);
250
251 // Create ADTS header
252 const header = AdtsHeader{
253 .profile = @intCast(@intFromEnum(self.profile) - 1),
254 .sampling_frequency_index = @intFromEnum(SampleRateIndex.fromRate(self.sample_rate)),
255 .channel_configuration = @intCast(@intFromEnum(ChannelConfig.fromChannels(self.channels))),
256 .frame_length = frame_length,
257 };
258
259 // Write header
260 const header_bytes = header.encode();
261 try self.output.appendSlice(self.allocator, &header_bytes);
262
263 // Write data
264 try self.output.appendSlice(self.allocator, frame_data.items);
265
266 self.frame_count += 1;
267 }
268
269 fn encodeRawDataBlock(self: *Self, samples: []const f32, data: *std.ArrayList(u8)) !void {
270 // Simplified encoding: just store quantized spectral data
271 // Real AAC encoding involves:
272 // 1. Psychoacoustic analysis
273 // 2. TNS (Temporal Noise Shaping)
274 // 3. M/S stereo coding
275 // 4. Intensity stereo
276 // 5. Scale factor band grouping
277 // 6. Huffman coding
278
279 // Mix to mono for simplified encoding
280 var mono_samples: [SAMPLES_PER_FRAME]f32 = undefined;
281 for (0..SAMPLES_PER_FRAME) |i| {
282 if (self.channels == 2 and i * 2 + 1 < samples.len) {
283 mono_samples[i] = (samples[i * 2] + samples[i * 2 + 1]) * 0.5;
284 } else if (i * self.channels < samples.len) {
285 mono_samples[i] = samples[i * self.channels];
286 } else {
287 mono_samples[i] = 0;
288 }
289 }
290
291 // Apply simple windowing (sine window)
292 var windowed: [SAMPLES_PER_FRAME * 2]f32 = undefined;
293 @memcpy(windowed[0..SAMPLES_PER_FRAME], self.prev_samples[0..SAMPLES_PER_FRAME]);
294 @memcpy(windowed[SAMPLES_PER_FRAME..], &mono_samples);
295 @memcpy(self.prev_samples[0..SAMPLES_PER_FRAME], &mono_samples);
296
297 for (0..SAMPLES_PER_FRAME * 2) |i| {
298 const w = @sin(math.pi / (2.0 * SAMPLES_PER_FRAME * 2) * (@as(f32, @floatFromInt(i)) + 0.5));
299 windowed[i] *= w;
300 }
301
302 // Simplified MDCT output (just store envelope)
303 const bytes_per_frame = @max(32, self.bitrate * SAMPLES_PER_FRAME / self.sample_rate / 8);
304
305 for (0..bytes_per_frame) |i| {
306 // Simple spectral envelope approximation
307 const start = i * SAMPLES_PER_FRAME / bytes_per_frame;
308 const end = (i + 1) * SAMPLES_PER_FRAME / bytes_per_frame;
309
310 var energy: f32 = 0;
311 for (start..end) |j| {
312 energy += windowed[j] * windowed[j];
313 }
314 energy = @sqrt(energy / @as(f32, @floatFromInt(end - start)));
315
316 const quantized: u8 = @intFromFloat(std.math.clamp(energy * 255.0, 0, 255));
317 try data.append(self.allocator, quantized);
318 }
230 _ = self;
231 _ = samples;
232 return error.UnsupportedLegacyEncoder;
319233 }
320234
321235 /// Finalize and return encoded AAC data
@@ -357,8 +271,8 @@ pub const AudioSpecificConfig = struct {
357271 var config: [2]u8 = undefined;
358272
359273 const aot = self.audio_object_type.getAudioObjectType();
360 const sri = @intFromEnum(SampleRateIndex.fromRate(self.sample_rate));
361 const cc = @intFromEnum(ChannelConfig.fromChannels(self.channels));
274 const sri = @as(u4, @bitCast(SampleRateIndex.fromRate(self.sample_rate)));
275 const cc = @backingInt(ChannelConfig.fromChannels(self.channels));
362276
363277 // audioObjectType (5 bits) + samplingFrequencyIndex (4 bits)
364278 config[0] = @as(u8, aot) << 3;
@@ -397,8 +311,8 @@ test "AacEncoder encode" {
397311 samples[i] = @sin(t * 440 * 2 * math.pi);
398312 }
399313
400 try encoder.encode(&samples);
401 try std.testing.expect(encoder.getFrameCount() > 0);
314 try std.testing.expectError(error.UnsupportedLegacyEncoder, encoder.encode(&samples));
315 try std.testing.expectEqual(@as(u32, 0), encoder.getFrameCount());
402316}
403317
404318test "AdtsHeader encode" {
@@ -424,3 +338,12 @@ test "AudioSpecificConfig encode" {
424338 const bytes = config.encode();
425339 try std.testing.expect(bytes[0] != 0); // Should have content
426340}
341test "legacy AAC allocation failure releases earlier buffers" {
342 const Check = struct {
343 fn run(allocator: Allocator) !void {
344 var encoder = try AacEncoder.init(allocator, 48000, 2, .medium);
345 defer encoder.deinit();
346 }
347 };
348 try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{});
349}
packages/audio/src/encoding/encoding.zigmodified+5-1
Changes to packages/audio/src/encoding/encoding.zig
@@ -1,5 +1,5 @@
11// Home Audio Library - Encoding Module
2// Pure Zig audio encoders for MP3, AAC, and Opus
2// Audio encoder interfaces and an opt-in native AAC-LC codec.
33
44pub const mp3_encoder = ;
55pub const aac_encoder = ;
@@ -33,3 +33,7 @@ test "encoding module" {
3333 _ = aac_encoder;
3434 _ = opus_encoder;
3535}
36
37/// Header-checked AAC-LC packets, explicit sample counts and priming metadata.
38pub const native_aac = ;
39pub const NativeAacEncoder = native_aac.Encoder;
packages/audio/src/encoding/native_aac.cadded+66-0
Changes to packages/audio/src/encoding/native_aac.c
@@ -0,0 +1,66 @@
1/* Home header-checked, bounded AAC-LC adapter. No FFmpeg/Libav. */
2#include "native_aac.h"
3/* FDK publishes unused static helpers in its public C header. */
4#pragma GCC diagnostic push
5#pragma GCC diagnostic ignored "-Wunused-function"
6#include <fdk-aac/aacenc_lib.h>
7#pragma GCC diagnostic pop
8#include <stdlib.h>
9#include <string.h>
10_Static_assert(sizeof(INT_PCM)==2,"FDK must use 16-bit input samples");
11struct home_aac { HANDLE_AACENCODER codec;uint32_t channels;int draining,ended,failed; };
12void home_aac_close(home_aac *s) {
13 if(!s)return;
14 if(s->codec)aacEncClose(&s->codec);
15 free(s);
16}
17int home_aac_open(const home_aac_config *cfg,home_aac **out,home_aac_info *info) {
18 if(!out || !info)return -1;
19 *out=NULL;memset(info,0,sizeof(*info));
20 if(!cfg || cfg->channels<1 || cfg->channels>2 || cfg->bitrate<16000 || cfg->bitrate>320000)return -1;
21 switch(cfg->sample_rate) {case 8000:case 11025:case 12000:case 16000:case 22050:case 24000:case 32000:case 44100:case 48000:break;default:return -1;}
22 home_aac *s=calloc(1,sizeof(*s));if(!s)return -2;
23 s->channels=cfg->channels;
24 if(aacEncOpen(&s->codec,1,cfg->channels)!=AACENC_OK)goto fail;
25 if(aacEncoder_SetParam(s->codec,AACENC_AOT,2)!=AACENC_OK ||
26 aacEncoder_SetParam(s->codec,AACENC_SAMPLERATE,cfg->sample_rate)!=AACENC_OK ||
27 aacEncoder_SetParam(s->codec,AACENC_CHANNELMODE,cfg->channels==1?MODE_1:MODE_2)!=AACENC_OK ||
28 aacEncoder_SetParam(s->codec,AACENC_CHANNELORDER,1)!=AACENC_OK ||
29 aacEncoder_SetParam(s->codec,AACENC_BITRATE,cfg->bitrate)!=AACENC_OK ||
30 aacEncoder_SetParam(s->codec,AACENC_TRANSMUX,TT_MP4_RAW)!=AACENC_OK ||
31 aacEncoder_SetParam(s->codec,AACENC_AFTERBURNER,1)!=AACENC_OK ||
32 aacEncEncode(s->codec,NULL,NULL,NULL,NULL)!=AACENC_OK)goto fail;
33 AACENC_InfoStruct native={0};
34 if(aacEncInfo(s->codec,&native)!=AACENC_OK || native.frameLength!=1024 || native.confSize!=2 || native.maxOutBufBytes>8192 || native.inputChannels!=cfg->channels || native.nDelay>16384)goto fail;
35 info->frame_length=native.frameLength;info->delay=native.nDelay;info->config_size=native.confSize;
36 memcpy(info->config,native.confBuf,native.confSize);*out=s;return 0;
37fail:
38 home_aac_close(s);return -3;
39}
40static int run(home_aac *s,const uint8_t *pcm,uint32_t frames,int flush,uint8_t *data,uint32_t capacity,uint32_t *bytes,uint32_t *consumed) {
41 if(!s || !data || !bytes || !consumed || capacity<8192 || capacity>0x7fffffff || (!flush && (!pcm || !frames || frames>1024)))return -1;
42 *bytes=0;*consumed=0;
43 if(s->failed || (!flush && s->draining))return -3;
44 if(s->ended)return 1;
45 INT_PCM samples[2048];
46 if(!flush)for(uint32_t i=0;i<frames*s->channels;i++) {
47 uint16_t value=(uint16_t)pcm[2*i]|(uint16_t)((uint16_t)pcm[2*i+1]<<8);
48 memcpy(&samples[i],&value,2);
49 }
50 void *input=samples,*output=data;
51 INT input_id=IN_AUDIO_DATA,output_id=OUT_BITSTREAM_DATA;
52 INT input_size=(INT)(frames*s->channels*2),output_size=(INT)capacity,input_element=2,output_element=1;
53 AACENC_BufDesc in={1,&input,&input_id,&input_size,&input_element};
54 AACENC_BufDesc out={1,&output,&output_id,&output_size,&output_element};
55 AACENC_InArgs args={0};AACENC_OutArgs result={0};
56 args.numInSamples=flush?-1:(INT)(frames*s->channels);
57 if(flush)s->draining=1;
58 AACENC_ERROR error=aacEncEncode(s->codec,&in,&out,&args,&result);
59 if(error==AACENC_ENCODE_EOF){s->ended=1;return 1;}
60 if(error!=AACENC_OK || result.numOutBytes<0 || result.numOutBytes>(INT)capacity || result.numInSamples<0 || (!flush && ((uint32_t)result.numInSamples>frames*s->channels || (uint32_t)result.numInSamples%s->channels))) {s->failed=1;return -3;}
61 *bytes=(uint32_t)result.numOutBytes;*consumed=(uint32_t)result.numInSamples/s->channels;
62 if(!flush && !*bytes && !*consumed){s->failed=1;return -3;}
63 return 0;
64}
65int home_aac_encode(home_aac *s,const uint8_t *pcm,uint32_t frames,uint8_t *data,uint32_t capacity,uint32_t *bytes,uint32_t *consumed) {return run(s,pcm,frames,0,data,capacity,bytes,consumed);}
66int home_aac_flush(home_aac *s,uint8_t *data,uint32_t capacity,uint32_t *bytes) {uint32_t consumed=0;return run(s,NULL,0,1,data,capacity,bytes,&consumed);}
packages/audio/src/encoding/native_aac.hadded+15-0
Changes to packages/audio/src/encoding/native_aac.h
@@ -0,0 +1,15 @@
1/* Home AAC-LC adapter, MIT. FDK AAC is an optional separately licensed codec. */
2#ifndef HOME_NATIVE_AAC_H
3#define HOME_NATIVE_AAC_H
4#include <stdint.h>
5typedef struct home_aac home_aac;
6typedef struct { uint32_t sample_rate,channels,bitrate; } home_aac_config;
7typedef struct { uint32_t frame_length,delay,config_size;uint8_t config[64]; } home_aac_info;
8int home_aac_open(const home_aac_config *,home_aac **,home_aac_info *);
9/* S16LE interleaved PCM; <=1024 frames/call. Output is raw AAC, not ADTS.
10 * 0 success, negative invalid/allocation/codec error. No implicit partial drop. */
11int home_aac_encode(home_aac *,const uint8_t *,uint32_t frames,uint8_t *,uint32_t capacity,uint32_t *bytes,uint32_t *consumed);
12/* 1 EOF, 0 progress. Drain until EOF, then close. Never encode after draining. */
13int home_aac_flush(home_aac *,uint8_t *,uint32_t capacity,uint32_t *bytes);
14void home_aac_close(home_aac *);
15#endif
packages/audio/src/encoding/native_aac.zigadded+63-0
Changes to packages/audio/src/encoding/native_aac.zig
@@ -0,0 +1,63 @@
1//! Opt-in AAC-LC using Home's header-checked standalone FDK boundary.
2//! Serialize calls. Packet storage is borrowed until the next encode/flush call.
3const Handle = opaque {};
4pub const Config = extern struct { sample_rate: u32, channels: u32, bitrate: u32 };
5pub const Info = extern struct { frame_length: u32, delay: u32, config_size: u32, config: [64]u8 };
6extern fn home_aac_open(*const Config, *?*Handle, *Info) c_int;
7extern fn home_aac_encode(*Handle, [*]const u8, u32, [*]u8, u32, *u32, *u32) c_int;
8extern fn home_aac_flush(*Handle, [*]u8, u32, *u32) c_int;
9extern fn home_aac_close(*Handle) void;
10pub const Result = struct { consumed_frames: u32, data: []const u8 };
11pub const Encoder = struct {
12 handle: *Handle,
13 config: Config,
14 info: Info,
15 output: [8192]u8 = undefined,
16 draining: bool = false,
17 failed: bool = false,
18 pub fn init(config: Config) !Encoder {
19 var handle: ?*Handle = null;
20 var info: Info = undefined;
21 try check(home_aac_open(&config, &handle, &info));
22 return .{ .handle = handle.?, .config = config, .info = info };
23 }
24 pub fn deinit(s: *Encoder) void {
25 home_aac_close(s.handle);
26 }
27 pub fn encode(s: *Encoder, data: []const u8) !Result {
28 if (s.failed or s.draining) return error.EncoderFailed;
29 const stride = s.config.channels * 2;
30 if (data.len == 0 or data.len % stride != 0 or data.len / stride > 1024) return error.InvalidPcm;
31 var bytes: u32 = 0;
32 var consumed: u32 = 0;
33 check(home_aac_encode(s.handle, data.ptr, @intCast(data.len / stride), &s.output, s.output.len, &bytes, &consumed)) catch |err| {
34 s.failed = true;
35 return err;
36 };
37 return .{ .consumed_frames = consumed, .data = s.output[0..bytes] };
38 }
39 pub fn flush(s: *Encoder) !?[]const u8 {
40 if (s.failed) return error.EncoderFailed;
41 s.draining = true;
42 for (0..16) |_| {
43 var bytes: u32 = 0;
44 const rc = home_aac_flush(s.handle, &s.output, s.output.len, &bytes);
45 if (rc == 1) return null;
46 check(rc) catch |err| {
47 s.failed = true;
48 return err;
49 };
50 if (bytes != 0) return s.output[0..bytes];
51 }
52 s.failed = true;
53 return error.EncoderFailed;
54 }
55};
56fn check(rc: c_int) !void {
57 return switch (rc) {
58 0 => {},
59 -1 => error.InvalidConfiguration,
60 -2 => error.OutOfMemory,
61 else => error.EncoderFailed,
62 };
63}
packages/audio/src/encoding/native_aac_test.zigadded+25-0
Changes to packages/audio/src/encoding/native_aac_test.zig
@@ -0,0 +1,25 @@
1const std = ;
2const Encoder = .Encoder;
3test "AAC-LC partial PCM, exact configuration, delayed drain and state checks" {
4 var encoder = try Encoder.init(.{ .sample_rate = 48000, .channels = 2, .bitrate = 128000 });
5 defer encoder.deinit();
6 try std.testing.expectEqual(@as(u32, 1024), encoder.info.frame_length);
7 try std.testing.expectEqualSlices(u8, &.{ 0x11, 0x90 }, encoder.info.config[0..2]);
8 try std.testing.expect(encoder.info.delay > 0);
9 var samples: [4096]u8 = undefined;
10 @memset(&samples, 0);
11 try std.testing.expectError(error.InvalidPcm, encoder.encode(samples[0..3]));
12 var frames: usize = 0;
13 var packets: usize = 0;
14 while (frames < 2500) {
15 const count: usize = @min(@as(usize, 137), 2500 - frames);
16 const result = try encoder.encode(samples[0 .. count * 4]);
17 frames += result.consumed_frames;
18 if (result.data.len != 0) packets += 1;
19 }
20 while (try encoder.flush()) |_| packets += 1;
21 try std.testing.expect(packets * 1024 >= 2500 + encoder.info.delay);
22 try std.testing.expectError(error.EncoderFailed, encoder.encode(samples[0..4]));
23 try std.testing.expect((try encoder.flush()) == null);
24 try std.testing.expectError(error.InvalidConfiguration, Encoder.init(.{ .sample_rate = 12345, .channels = 1, .bitrate = 64000 }));
25}
packages/video/src/codecs/audio/aac_encoder.zigmodified+12-363
Changes to packages/video/src/codecs/audio/aac_encoder.zig
@@ -1,375 +1,24 @@
1//! Legacy compatibility entry point. Its old fixed-width spectral payload was
2//! not an AAC bitstream. Use packages/audio/src/encoding/native_aac.zig instead.
13const std = ;
2const aac = ;
3const frame = ;
4const err = ;
5
6const AudioFrame = frame.AudioFrame;
7const VideoError = err.VideoError;
8const AudioSpecificConfig = aac.AudioSpecificConfig;
9
10/// Full AAC encoder implementation
11/// Implements AAC-LC encoding with MDCT, psychoacoustic model, quantization, and Huffman coding
4const AudioFrame = .AudioFrame;
125pub const AacFullEncoder = struct {
136 allocator: std.mem.Allocator,
14 config: AudioSpecificConfig,
15 bitrate: u32,
167 sample_rate: u32,
178 channels: u8,
18
19 // MDCT state
20 mdct_size: usize,
21 mdct_coeffs: [2][1024]f32,
22 previous_samples: [2][1024]f32,
23
24 // Psychoacoustic model
25 masking_thresholds: [49]f32, // Per scalefactor band
26
27 // Quantization
28 quantized_coeffs: [2][1024]i16,
29 scalefactors: [2][49]u8,
30
31 // Huffman tables (simplified)
32 huffman_codebook: [11][256]HuffmanCode,
33
34 const Self = @This();
35
36 const HuffmanCode = struct {
37 code: u32,
38 length: u8,
39 };
40
41 pub fn init(allocator: std.mem.Allocator, sample_rate: u32, channels: u8, bitrate: u32) Self {
42 var encoder = Self{
43 .allocator = allocator,
44 .config = AudioSpecificConfig.defaultLC(sample_rate, channels),
45 .bitrate = bitrate,
46 .sample_rate = sample_rate,
47 .channels = channels,
48 .mdct_size = 1024,
49 .mdct_coeffs = undefined,
50 .previous_samples = undefined,
51 .masking_thresholds = undefined,
52 .quantized_coeffs = undefined,
53 .scalefactors = undefined,
54 .huffman_codebook = undefined,
55 };
56
57 @memset(&encoder.previous_samples[0], 0.0);
58 @memset(&encoder.previous_samples[1], 0.0);
59
60 encoder.initHuffmanTables();
61
62 return encoder;
63 }
64
65 pub fn deinit(self: *Self) void {
66 _ = self;
67 }
68
69 /// Encode audio frame to raw AAC data (no container)
70 pub fn encode(self: *Self, audio_frame: *const AudioFrame) ![]u8 {
71 // AAC encoding pipeline:
72 // 1. Window and MDCT
73 // 2. Psychoacoustic model
74 // 3. Quantization with iterative loop
75 // 4. Huffman coding
76 // 5. Bitstream packing
77
78 // Step 1: Apply MDCT for each channel
79 for (0..self.channels) |ch| {
80 try self.applyMdct(audio_frame, @intCast(ch));
81 }
82
83 // Step 2: Psychoacoustic model (calculate masking thresholds)
84 self.calculateMaskingThresholds();
85
86 // Step 3: Quantize coefficients
87 for (0..self.channels) |ch| {
88 try self.quantizeChannel(@intCast(ch));
89 }
90
91 // Step 4 & 5: Huffman encode and pack bitstream
92 return try self.packBitstream();
93 }
94
95 /// Encode with ADTS header
96 pub fn encodeAdts(self: *Self, audio_frame: *const AudioFrame) ![]u8 {
97 // Get raw AAC data
98 const aac_data = try self.encode(audio_frame);
99 defer self.allocator.free(aac_data);
100
101 // ADTS header is 7 bytes (or 9 with CRC)
102 const adts_header_size = 7;
103 var result = try self.allocator.alloc(u8, adts_header_size + aac_data.len);
104
105 // Build ADTS header
106 const profile = 1; // AAC-LC
107 const freq_index = self.getSampleRateIndex();
108 const chan_config = self.channels;
109 const frame_length = @as(u16, @intCast(adts_header_size + aac_data.len));
110
111 // Syncword (12 bits): 0xFFF
112 result[0] = 0xFF;
113 result[1] = 0xF0; // Top 4 bits of syncword
114
115 // MPEG-4, Layer=0, no CRC (1 bit each)
116 result[1] |= 0x01; // MPEG-4
117
118 // Profile (2 bits), freq index (4 bits), private (1 bit), chan config (3 bits start)
119 result[2] = (@as(u8, @intCast(profile)) << 6) | (@as(u8, @intCast(freq_index)) << 2);
120 result[3] = (@as(u8, @intCast(chan_config)) << 6);
121
122 // Frame length (13 bits) spans bytes 3-5
123 result[3] |= @intCast((frame_length >> 11) & 0x03);
124 result[4] = @intCast((frame_length >> 3) & 0xFF);
125 result[5] = @intCast((frame_length & 0x07) << 5);
126
127 // Buffer fullness (11 bits) = 0x7FF (VBR)
128 result[5] |= 0x1F;
129 result[6] = 0xFC;
130
131 // Copy AAC data
132 @memcpy(result[adts_header_size..], aac_data);
133
134 return result;
135 }
136
137 fn applyMdct(self: *Self, audio_frame: *const AudioFrame, channel: u8) !void {
138 // Get input samples
139 var input_samples: [2048]f32 = undefined;
140
141 // Copy previous samples (for overlap)
142 @memcpy(input_samples[0..1024], &self.previous_samples[channel]);
143
144 // Copy new samples
145 for (0..1024) |i| {
146 if (i < audio_frame.num_samples) {
147 input_samples[1024 + i] = audio_frame.getSampleF32(channel, @intCast(i)) orelse 0.0;
148 } else {
149 input_samples[1024 + i] = 0.0;
150 }
151 }
152
153 // Save for next frame
154 @memcpy(&self.previous_samples[channel], input_samples[1024..2048]);
155
156 // Apply window (Kaiser-Bessel Derived window)
157 var windowed: [2048]f32 = undefined;
158 for (0..2048) |i| {
159 const window_val = self.kbdWindow(i, 2048);
160 windowed[i] = input_samples[i] * window_val;
161 }
162
163 // MDCT: X[k] = sum(n=0..2N-1) { x[n] * cos(π/N * (n + 0.5 + N/2) * (k + 0.5)) }
164 const N = 1024;
165 for (0..N) |k| {
166 var sum: f32 = 0.0;
167 for (0..2 * N) |n| {
168 const arg = (std.math.pi / @as(f32, @floatFromInt(N))) *
169 (@as(f32, @floatFromInt(n)) + 0.5 + @as(f32, @floatFromInt(N)) / 2.0) *
170 (@as(f32, @floatFromInt(k)) + 0.5);
171 sum += windowed[n] * @cos(arg);
172 }
173 self.mdct_coeffs[channel][k] = sum;
174 }
9 bitrate: u32,
10 pub fn init(allocator: std.mem.Allocator, sample_rate: u32, channels: u8, bitrate: u32) AacFullEncoder {
11 return .{ .allocator = allocator, .sample_rate = sample_rate, .channels = channels, .bitrate = bitrate };
17512 }
176
177 fn kbdWindow(self: *const Self, n: usize, length: usize) f32 {
13 pub fn deinit(self: *AacFullEncoder) void {
17814 _ = self;
179 // Simplified Kaiser-Bessel Derived window
180 // For production, would use proper KBD formula with Bessel functions
181 const x = @as(f32, @floatFromInt(n)) / @as(f32, @floatFromInt(length));
182 return @sin(std.math.pi * x);
183 }
184
185 fn calculateMaskingThresholds(self: *Self) void {
186 // Simplified psychoacoustic model
187 // Real AAC uses complex perceptual modeling with:
188 // - Bark scale frequency mapping
189 // - Spreading function
190 // - Absolute threshold of hearing
191 // - Simultaneous masking
192
193 // For now, use simplified per-band thresholds
194 for (0..49) |band| {
195 // Lower bands need more bits (higher masking threshold)
196 const band_f = @as(f32, @floatFromInt(band)) / 49.0;
197 self.masking_thresholds[band] = 0.1 + band_f * 0.4;
198 }
199 }
200
201 fn quantizeChannel(self: *Self, channel: u8) !void {
202 // AAC quantization: x_q = sign(x) * floor((|x| / 2^(scalefactor/4))^(3/4) + 0.4054)
203 // Iterative rate-distortion loop to find optimal scalefactors
204
205 // Scalefactor bands for 48kHz (simplified - would use proper tables)
206 const num_bands = 49;
207 var band_start: [50]usize = undefined;
208 for (0..50) |i| {
209 band_start[i] = (i * 1024) / 49;
210 }
211
212 // Calculate initial scalefactors
213 for (0..num_bands) |band| {
214 const start = band_start[band];
215 const end = band_start[band + 1];
216
217 // Find max coefficient in band
218 var max_coeff: f32 = 0.0;
219 for (start..end) |i| {
220 const abs_coeff = @abs(self.mdct_coeffs[channel][i]);
221 max_coeff = @max(max_coeff, abs_coeff);
222 }
223
224 // Calculate scalefactor to keep quantized values in range
225 if (max_coeff > 0.0) {
226 const target_max: f32 = 8191.0; // Max quantized value
227 const sf = std.math.log2(max_coeff / target_max) * 4.0;
228 self.scalefactors[channel][band] = @intFromFloat(@max(0.0, @min(255.0, sf)));
229 } else {
230 self.scalefactors[channel][band] = 0;
231 }
232 }
233
234 // Quantize coefficients
235 for (0..num_bands) |band| {
236 const start = band_start[band];
237 const end = band_start[band + 1];
238 const sf = self.scalefactors[channel][band];
239 const scale = std.math.pow(f32, 2.0, @as(f32, @floatFromInt(sf)) / 4.0);
240
241 for (start..end) |i| {
242 const x = self.mdct_coeffs[channel][i];
243 const sign: f32 = if (x >= 0.0) 1.0 else -1.0;
244 const abs_x = @abs(x);
245
246 // Quantization formula
247 const normalized = abs_x / scale;
248 const powered = std.math.pow(f32, normalized, 0.75); // 3/4 power
249 const quantized_f = powered + 0.4054;
250
251 var quantized: i16 = @intFromFloat(@min(8191.0, quantized_f));
252 if (sign < 0.0) quantized = -quantized;
253
254 self.quantized_coeffs[channel][i] = quantized;
255 }
256 }
25715 }
258
259 fn packBitstream(self: *Self) ![]u8 {
260 // Simplified bitstream packing
261 // Real AAC has complex bitstream with:
262 // - Individual channel stream (ICS)
263 // - Section data (Huffman codebook selection)
264 // - Scalefactor data
265 // - Spectral data (Huffman coded)
266
267 // Estimate size (scalefactors + quantized data)
268 const estimated_size = 1024 + (self.channels * 512);
269 const buffer = try self.allocator.alloc(u8, estimated_size);
270 @memset(buffer, 0);
271
272 var bit_offset: usize = 0;
273
274 // Write element tag (SCE or CPE)
275 if (self.channels == 1) {
276 // Single Channel Element
277 self.writeBits(buffer, &bit_offset, 0, 3); // ID_SCE
278 } else {
279 // Channel Pair Element
280 self.writeBits(buffer, &bit_offset, 1, 3); // ID_CPE
281 }
282
283 // Write element instance tag
284 self.writeBits(buffer, &bit_offset, 0, 4);
285
286 // For each channel, write:
287 for (0..self.channels) |ch| {
288 // Window sequence (ONLY_LONG_SEQUENCE)
289 self.writeBits(buffer, &bit_offset, 0, 2);
290
291 // Window shape
292 self.writeBits(buffer, &bit_offset, 0, 1);
293
294 // Max sfb (number of scalefactor bands)
295 self.writeBits(buffer, &bit_offset, 49, 6);
296
297 // Scalefactors (DPCM coded)
298 var prev_sf: i32 = 0;
299 for (0..49) |band| {
300 const sf: i32 = @intCast(self.scalefactors[ch][band]);
301 const delta = sf - prev_sf;
302 // Write delta (simplified - would use proper Huffman coding)
303 self.writeBits(buffer, &bit_offset, @bitCast(@as(i8, @intCast(std.math.clamp(delta, -60, 60)))), 8);
304 prev_sf = sf;
305 }
306
307 // Spectral data (simplified - would use Huffman coding per section)
308 for (0..1024) |i| {
309 const coeff = self.quantized_coeffs[ch][i];
310 // Write coefficient (simplified)
311 self.writeBits(buffer, &bit_offset, @bitCast(@as(i16, coeff)), 13);
312 }
313 }
314
315 // Pad to byte boundary
316 const byte_size = (bit_offset + 7) / 8;
317 return self.allocator.realloc(buffer, byte_size);
318 }
319
320 fn writeBits(self: *const Self, buffer: []u8, bit_offset: *usize, value: anytype, num_bits: u6) void {
16 pub fn encode(self: *AacFullEncoder, input: *const AudioFrame) ![]u8 {
32117 _ = self;
322 const T = @TypeOf(value);
323 const val: u32 = if (@typeInfo(T) == .Int) @bitCast(@as(i32, value)) else @intCast(value);
324
325 var bits_written: u6 = 0;
326 while (bits_written < num_bits) {
327 const byte_idx = bit_offset.* / 8;
328 const bit_idx = @as(u3, @intCast(bit_offset.* % 8));
329 const bits_in_byte = 8 - bit_idx;
330 const bits_to_write = @min(bits_in_byte, num_bits - bits_written);
331
332 const shift = num_bits - bits_written - bits_to_write;
333 const mask: u32 = (@as(u32, 1) << bits_to_write) - 1;
334 const bits = (val >> @intCast(shift)) & mask;
335
336 buffer[byte_idx] |= @intCast(bits << @intCast(bits_in_byte - bits_to_write));
337
338 bit_offset.* += bits_to_write;
339 bits_written += bits_to_write;
340 }
341 }
342
343 fn initHuffmanTables(self: *Self) void {
344 // Initialize simplified Huffman tables
345 // Real AAC uses 11 different codebooks with complex VLC tables
346 for (0..11) |book| {
347 for (0..256) |symbol| {
348 // Simplified: use symbol as code with fixed length
349 self.huffman_codebook[book][symbol] = .{
350 .code = @intCast(symbol),
351 .length = 8,
352 };
353 }
354 }
18 _ = input;
19 return error.UnsupportedLegacyEncoder;
35520 }
356
357 fn getSampleRateIndex(self: *const Self) u4 {
358 return switch (self.sample_rate) {
359 96000 => 0,
360 88200 => 1,
361 64000 => 2,
362 48000 => 3,
363 44100 => 4,
364 32000 => 5,
365 24000 => 6,
366 22050 => 7,
367 16000 => 8,
368 12000 => 9,
369 11025 => 10,
370 8000 => 11,
371 7350 => 12,
372 else => 3, // Default to 48kHz
373 };
21 pub fn encodeAdts(self: *AacFullEncoder, input: *const AudioFrame) ![]u8 {
22 return self.encode(input);
37423 }
37524};