also looking at this
feat(audio): add verified AAC-LC encoding and reject invalid prototype output
#918
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.
| @@ -0,0 +1,34 @@ | ||
| 1 | name: Native AAC-LC | |
| 2 | on: | |
| 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'] | |
| 7 | permissions: | |
| 8 | contents: read | |
| 9 | jobs: | |
| 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 | |
| @@ -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. | |
| 4 | It links the standalone FDK AAC library directly, without FFmpeg/Libav. The C | |
| 5 | boundary uses the installed headers and opaque handles; no FDK layouts are | |
| 6 | redeclared in Zig. Home's adapter is MIT; the FDK library retains its own license. | |
| 7 | ||
| 8 | The older `AacEncoder` and `AacFullEncoder` prototypes emitted envelope bytes or | |
| 9 | fixed-width coefficients as if they were AAC. Their encode entry points now | |
| 10 | return `UnsupportedLegacyEncoder`. Configuration helpers remain available; | |
| 11 | use `audio.NativeAacEncoder` or `audio.encoding.native_aac.Encoder` for real AAC. | |
| 12 | The old prototype's partial-allocation cleanup is also fixed. | |
| 13 | ||
| 14 | Compile `native_aac.c` with the system compiler, include the installed FDK header | |
| 15 | root and link `fdk-aac`. Instantiate with an explicit sample rate, mono/stereo | |
| 16 | channels and bitrate in bits/s. Supported rates are 8, 11.025, 12, 16, 22.05, 24, | |
| 17 | 32, 44.1 and 48kHz. Bitrate range is 16–320kb/s; unsupported combinations fail. | |
| 18 | No 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 | |
| 21 | plus an optional raw access unit. Retain and feed any unconsumed input. Borrowed | |
| 22 | packet bytes expire at the next call. Fixed input/output buffers bound memory; | |
| 23 | there is no frame-sized growing history. Serialize calls on each encoder. | |
| 24 | ||
| 25 | The returned `Info` supplies the real AudioSpecificConfig, frame length and | |
| 26 | codec delay in samples. Keep it with the stream. Drain `flush` until `null`, then | |
| 27 | close. Encoding after drain fails. A codec failure poisons subsequent calls. | |
| 28 | The muxer must account for codec delay and final zero padding; do not timestamp | |
| 29 | packets from callback time or assume the codec delay is 1024/2048 samples. | |
| 30 | ||
| 31 | Validation uses FDK 2.0.3 at commit | |
| 32 | `716f4394641d53f0d79c9ddac3fa93b03a49f278`. Targeted tests exercise partial PCM, | |
| 33 | configuration bytes, delayed drain, padding coverage and state rejection on Zig | |
| 34 | 0.15.2 and Home's development toolchain. Independent camera integration decodes | |
| 35 | with FAAD2 and checks waveforms, segment sample counts and MP4 edit timelines. | |
| 36 | See `.github/workflows/native-aac.yml`; no hardware performance claim is made. | |
| 37 | ||
| 38 | ```sh | |
| 39 | zig test packages/audio/src/encoding/native_aac_test.zig \ | |
| 40 | packages/audio/src/encoding/native_aac.c -lc -lfdk-aac | |
| 41 | ``` | |
| @@ -339,7 +339,8 @@ pub const encoding = ; | ||
| 339 | 339 | pub const Mp3Encoder = encoding.Mp3Encoder; |
| 340 | 340 | pub const Mp3Quality = encoding.Mp3Quality; |
| 341 | 341 | pub const Id3v2Writer = encoding.Id3v2Writer; |
| 342 | pub const AacEncoder = encoding.AacEncoder; | |
| 342 | pub const AacEncoder = encoding.AacEncoder; // Legacy encode rejects; use NativeAacEncoder. | |
| 343 | pub const NativeAacEncoder = encoding.NativeAacEncoder; | |
| 343 | 344 | pub const AacQuality = encoding.AacQuality; |
| 344 | 345 | pub const AacProfile = encoding.AacProfile; |
| 345 | 346 | pub const OpusEncoder = encoding.OpusEncoder; |
| @@ -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,7 @@ 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 @as(u5, @bitCast(self)); | |
| 20 | 20 | } |
| 21 | 21 | }; |
| 22 | 22 | |
| @@ -198,6 +198,11 @@ pub const AacEncoder = struct { | ||
| 198 | 198 | quality: AacQuality, |
| 199 | 199 | profile: AacProfile, |
| 200 | 200 | ) !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); | |
| 201 | 206 | return Self{ |
| 202 | 207 | .allocator = allocator, |
| 203 | 208 | .sample_rate = sample_rate, |
| @@ -205,9 +210,9 @@ pub const AacEncoder = struct { | ||
| 205 | 210 | .bitrate = quality.getBitrate(), |
| 206 | 211 | .profile = profile, |
| 207 | 212 | .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, | |
| 211 | 216 | .frame_count = 0, |
| 212 | 217 | }; |
| 213 | 218 | } |
| @@ -219,103 +224,12 @@ pub const AacEncoder = struct { | ||
| 219 | 224 | self.allocator.free(self.scale_factors); |
| 220 | 225 | } |
| 221 | 226 | |
| 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. | |
| 223 | 229 | 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; | |
| 319 | 233 | } |
| 320 | 234 | |
| 321 | 235 | /// Finalize and return encoded AAC data |
| @@ -357,8 +271,8 @@ pub const AudioSpecificConfig = struct { | ||
| 357 | 271 | var config: [2]u8 = undefined; |
| 358 | 272 | |
| 359 | 273 | 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)); | |
| 362 | 276 | |
| 363 | 277 | // audioObjectType (5 bits) + samplingFrequencyIndex (4 bits) |
| 364 | 278 | config[0] = @as(u8, aot) << 3; |
| @@ -397,8 +311,8 @@ test "AacEncoder encode" { | ||
| 397 | 311 | samples[i] = @sin(t * 440 * 2 * math.pi); |
| 398 | 312 | } |
| 399 | 313 | |
| 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()); | |
| 402 | 316 | } |
| 403 | 317 | |
| 404 | 318 | test "AdtsHeader encode" { |
| @@ -424,3 +338,12 @@ test "AudioSpecificConfig encode" { | ||
| 424 | 338 | const bytes = config.encode(); |
| 425 | 339 | try std.testing.expect(bytes[0] != 0); // Should have content |
| 426 | 340 | } |
| 341 | test "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 | } | |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | // 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. | |
| 3 | 3 | |
| 4 | 4 | pub const mp3_encoder = ; |
| 5 | 5 | pub const aac_encoder = ; |
| @@ -33,3 +33,7 @@ test "encoding module" { | ||
| 33 | 33 | _ = aac_encoder; |
| 34 | 34 | _ = opus_encoder; |
| 35 | 35 | } |
| 36 | ||
| 37 | /// Header-checked AAC-LC packets, explicit sample counts and priming metadata. | |
| 38 | pub const native_aac = ; | |
| 39 | pub const NativeAacEncoder = native_aac.Encoder; | |
| @@ -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"); | |
| 11 | struct home_aac { HANDLE_AACENCODER codec;uint32_t channels;int draining,ended,failed; }; | |
| 12 | void home_aac_close(home_aac *s) { | |
| 13 | if(!s)return; | |
| 14 | if(s->codec)aacEncClose(&s->codec); | |
| 15 | free(s); | |
| 16 | } | |
| 17 | int 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; | |
| 37 | fail: | |
| 38 | home_aac_close(s);return -3; | |
| 39 | } | |
| 40 | static 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 | } | |
| 65 | int 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);} | |
| 66 | int 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);} | |
| @@ -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> | |
| 5 | typedef struct home_aac home_aac; | |
| 6 | typedef struct { uint32_t sample_rate,channels,bitrate; } home_aac_config; | |
| 7 | typedef struct { uint32_t frame_length,delay,config_size;uint8_t config[64]; } home_aac_info; | |
| 8 | int 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. */ | |
| 11 | int 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. */ | |
| 13 | int home_aac_flush(home_aac *,uint8_t *,uint32_t capacity,uint32_t *bytes); | |
| 14 | void home_aac_close(home_aac *); | |
| 15 | #endif | |
| @@ -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. | |
| 3 | const Handle = opaque {}; | |
| 4 | pub const Config = extern struct { sample_rate: u32, channels: u32, bitrate: u32 }; | |
| 5 | pub const Info = extern struct { frame_length: u32, delay: u32, config_size: u32, config: [64]u8 }; | |
| 6 | extern fn home_aac_open(*const Config, *?*Handle, *Info) c_int; | |
| 7 | extern fn home_aac_encode(*Handle, [*]const u8, u32, [*]u8, u32, *u32, *u32) c_int; | |
| 8 | extern fn home_aac_flush(*Handle, [*]u8, u32, *u32) c_int; | |
| 9 | extern fn home_aac_close(*Handle) void; | |
| 10 | pub const Result = struct { consumed_frames: u32, data: []const u8 }; | |
| 11 | pub 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 | }; | |
| 56 | fn 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 | } | |
| @@ -0,0 +1,25 @@ | ||
| 1 | const std = ; | |
| 2 | const Encoder = .Encoder; | |
| 3 | test "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 | } | |
| @@ -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. | |
| 1 | 3 | const std = ; |
| 2 | const aac = ; | |
| 3 | const frame = ; | |
| 4 | const err = ; | |
| 5 | ||
| 6 | const AudioFrame = frame.AudioFrame; | |
| 7 | const VideoError = err.VideoError; | |
| 8 | const AudioSpecificConfig = aac.AudioSpecificConfig; | |
| 9 | ||
| 10 | /// Full AAC encoder implementation | |
| 11 | /// Implements AAC-LC encoding with MDCT, psychoacoustic model, quantization, and Huffman coding | |
| 4 | const AudioFrame = .AudioFrame; | |
| 12 | 5 | pub const AacFullEncoder = struct { |
| 13 | 6 | allocator: std.mem.Allocator, |
| 14 | config: AudioSpecificConfig, | |
| 15 | bitrate: u32, | |
| 16 | 7 | sample_rate: u32, |
| 17 | 8 | 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 }; | |
| 175 | 12 | } |
| 176 | ||
| 177 | fn kbdWindow(self: *const Self, n: usize, length: usize) f32 { | |
| 13 | pub fn deinit(self: *AacFullEncoder) void { | |
| 178 | 14 | _ = 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 | } | |
| 257 | 15 | } |
| 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 { | |
| 321 | 17 | _ = 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; | |
| 355 | 20 | } |
| 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); | |
| 374 | 23 | } |
| 375 | 24 | }; |