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
packages/audio/src/encoding/aac_encoder.zigmodified+55-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,14 @@ pub const AacProfile = enum(u5) {
1616 aac_scalable = 6,
1717
1818 pub fn getAudioObjectType(self: AacProfile) u5 {
19 return @intFromEnum(self);
19 return switch (self) {
20 .aac_main => 1,
21 .aac_lc => 2,
22 .aac_ssr => 3,
23 .aac_ltp => 4,
24 .sbr => 5,
25 .aac_scalable => 6,
26 };
2027 }
2128};
2229
@@ -198,6 +205,11 @@ pub const AacEncoder = struct {
198205 quality: AacQuality,
199206 profile: AacProfile,
200207 ) !Self {
208 const mdct_buffer = try allocator.alloc(f32, SAMPLES_PER_FRAME * 2);
209 errdefer allocator.free(mdct_buffer);
210 const prev_samples = try allocator.alloc(f32, SAMPLES_PER_FRAME * @as(usize, channels));
211 errdefer allocator.free(prev_samples);
212 const scale_factors = try allocator.alloc(u8, NUM_SCALE_FACTOR_BANDS);
201213 return Self{
202214 .allocator = allocator,
203215 .sample_rate = sample_rate,
@@ -205,9 +217,9 @@ pub const AacEncoder = struct {
205217 .bitrate = quality.getBitrate(),
206218 .profile = profile,
207219 .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),
220 .mdct_buffer = mdct_buffer,
221 .prev_samples = prev_samples,
222 .scale_factors = scale_factors,
211223 .frame_count = 0,
212224 };
213225 }
@@ -219,103 +231,12 @@ pub const AacEncoder = struct {
219231 self.allocator.free(self.scale_factors);
220232 }
221233
222 /// Encode PCM samples to AAC (ADTS format)
234 /// The old envelope payload was not AAC. Never return it as encoded audio.
235 /// Use native_aac.Encoder for verified raw AAC-LC packets and codec delay.
223236 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 }
237 _ = self;
238 _ = samples;
239 return error.UnsupportedLegacyEncoder;
319240 }
320241
321242 /// Finalize and return encoded AAC data
@@ -357,8 +278,27 @@ pub const AudioSpecificConfig = struct {
357278 var config: [2]u8 = undefined;
358279
359280 const aot = self.audio_object_type.getAudioObjectType();
360 const sri = @intFromEnum(SampleRateIndex.fromRate(self.sample_rate));
361 const cc = @intFromEnum(ChannelConfig.fromChannels(self.channels));
281 const sri: u4 = switch (self.sample_rate) {
282 96000 => 0,
283 88200 => 1,
284 64000 => 2,
285 48000 => 3,
286 44100 => 4,
287 32000 => 5,
288 24000 => 6,
289 22050 => 7,
290 16000 => 8,
291 12000 => 9,
292 11025 => 10,
293 8000 => 11,
294 7350 => 12,
295 else => 4,
296 };
297 const cc: u4 = switch (self.channels) {
298 1...6 => @intCast(self.channels),
299 8 => 7,
300 else => 2,
301 };
362302
363303 // audioObjectType (5 bits) + samplingFrequencyIndex (4 bits)
364304 config[0] = @as(u8, aot) << 3;
@@ -397,8 +337,8 @@ test "AacEncoder encode" {
397337 samples[i] = @sin(t * 440 * 2 * math.pi);
398338 }
399339
400 try encoder.encode(&samples);
401 try std.testing.expect(encoder.getFrameCount() > 0);
340 try std.testing.expectError(error.UnsupportedLegacyEncoder, encoder.encode(&samples));
341 try std.testing.expectEqual(@as(u32, 0), encoder.getFrameCount());
402342}
403343
404344test "AdtsHeader encode" {
@@ -424,3 +364,12 @@ test "AudioSpecificConfig encode" {
424364 const bytes = config.encode();
425365 try std.testing.expect(bytes[0] != 0); // Should have content
426366}
367test "legacy AAC allocation failure releases earlier buffers" {
368 const Check = struct {
369 fn run(allocator: Allocator) !void {
370 var encoder = try AacEncoder.init(allocator, 48000, 2, .medium);
371 defer encoder.deinit();
372 }
373 };
374 try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{});
375}