also looking at this
feat(audio): add verified AAC-LC encoding and reject invalid prototype output
#918
10 files
+318
-471
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | // 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). | |
| 4 | 4 | |
| 5 | 5 | const std = ; |
| 6 | 6 | const Allocator = std.mem.Allocator; |
| @@ -16,7 +16,14 @@ pub const AacProfile = enum(u5) { | ||
| 16 | 16 | aac_scalable = 6, |
| 17 | 17 | |
| 18 | 18 | 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 | }; | |
| 20 | 27 | } |
| 21 | 28 | }; |
| 22 | 29 | |
| @@ -198,6 +205,11 @@ pub const AacEncoder = struct { | ||
| 198 | 205 | quality: AacQuality, |
| 199 | 206 | profile: AacProfile, |
| 200 | 207 | ) !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); | |
| 201 | 213 | return Self{ |
| 202 | 214 | .allocator = allocator, |
| 203 | 215 | .sample_rate = sample_rate, |
| @@ -205,9 +217,9 @@ pub const AacEncoder = struct { | ||
| 205 | 217 | .bitrate = quality.getBitrate(), |
| 206 | 218 | .profile = profile, |
| 207 | 219 | .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, | |
| 211 | 223 | .frame_count = 0, |
| 212 | 224 | }; |
| 213 | 225 | } |
| @@ -219,103 +231,12 @@ pub const AacEncoder = struct { | ||
| 219 | 231 | self.allocator.free(self.scale_factors); |
| 220 | 232 | } |
| 221 | 233 | |
| 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. | |
| 223 | 236 | 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; | |
| 319 | 240 | } |
| 320 | 241 | |
| 321 | 242 | /// Finalize and return encoded AAC data |
| @@ -357,8 +278,27 @@ pub const AudioSpecificConfig = struct { | ||
| 357 | 278 | var config: [2]u8 = undefined; |
| 358 | 279 | |
| 359 | 280 | 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 | }; | |
| 362 | 302 | |
| 363 | 303 | // audioObjectType (5 bits) + samplingFrequencyIndex (4 bits) |
| 364 | 304 | config[0] = @as(u8, aot) << 3; |
| @@ -397,8 +337,8 @@ test "AacEncoder encode" { | ||
| 397 | 337 | samples[i] = @sin(t * 440 * 2 * math.pi); |
| 398 | 338 | } |
| 399 | 339 | |
| 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()); | |
| 402 | 342 | } |
| 403 | 343 | |
| 404 | 344 | test "AdtsHeader encode" { |
| @@ -424,3 +364,12 @@ test "AudioSpecificConfig encode" { | ||
| 424 | 364 | const bytes = config.encode(); |
| 425 | 365 | try std.testing.expect(bytes[0] != 0); // Should have content |
| 426 | 366 | } |
| 367 | test "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 | } | |