/* * plsconvert-fdk — a small WebAssembly wrapper around the Fraunhofer FDK AAC * encoder, giving the browser real AAC-LC, HE-AAC v1 and HE-AAC v2 encoding. * * Wrapper code (this file): MIT, part of PLSconvert.me. * The encoder it calls is the Fraunhofer FDK AAC Codec Library, used under its * own licence, whose complete text ships alongside this module in * FDK-AAC-NOTICE.txt as that licence requires. This wrapper is deliberately a * standalone module rather than a patched FFmpeg build: FFmpeg's inability to * ship libfdk_aac is a GPL-compatibility problem, not a restriction the FDK * licence itself imposes, and linking nothing GPL means the restriction never * arises. * * Input is interleaved 16-bit signed PCM. Output is an ADTS stream, which is a * valid .aac file on its own and can be muxed into .m4a without re-encoding. */ #include #include #include #include #include static unsigned char *g_out = NULL; static int g_out_len = 0; static char g_err[256]; static void fail(const char *what, AACENC_ERROR e) { snprintf(g_err, sizeof(g_err), "%s failed (fdk error 0x%04x)", what, (unsigned)e); } EMSCRIPTEN_KEEPALIVE unsigned char *fdk_out_ptr(void) { return g_out; } EMSCRIPTEN_KEEPALIVE int fdk_out_len(void) { return g_out_len; } EMSCRIPTEN_KEEPALIVE const char *fdk_err(void) { return g_err; } EMSCRIPTEN_KEEPALIVE void fdk_release(void) { free(g_out); g_out = NULL; g_out_len = 0; } /* Grow the output buffer geometrically so a long track does not realloc per frame. */ static int push(const unsigned char *data, int n, int *cap) { if (g_out_len + n > *cap) { int want = (*cap ? *cap * 2 : 1 << 16); while (want < g_out_len + n) want *= 2; unsigned char *bigger = realloc(g_out, want); if (!bigger) { snprintf(g_err, sizeof(g_err), "out of memory"); return 0; } g_out = bigger; *cap = want; } memcpy(g_out + g_out_len, data, n); g_out_len += n; return 1; } /* * aot 2 = AAC-LC, 5 = HE-AAC v1 (SBR), 29 = HE-AAC v2 (SBR + PS) * bitrate bits per second, used when vbr == 0 * vbr 0 = constant bitrate, 1..5 = FDK's VBR modes (1 lowest) * Returns 1 on success, 0 on failure with fdk_err() set. */ EMSCRIPTEN_KEEPALIVE int fdk_encode(const short *pcm, int frames, int sample_rate, int channels, int aot, int bitrate, int vbr) { HANDLE_AACENCODER enc = NULL; AACENC_InfoStruct info = {0}; AACENC_ERROR err; int cap = 0, ok = 0; fdk_release(); g_err[0] = '\0'; if (channels < 1 || channels > 2) { snprintf(g_err, sizeof(g_err), "this build supports mono and stereo only"); return 0; } if (aot == 29 && channels != 2) { snprintf(g_err, sizeof(g_err), "HE-AAC v2 requires stereo input"); return 0; } if ((err = aacEncOpen(&enc, 0, channels)) != AACENC_OK) { fail("aacEncOpen", err); return 0; } #define SET(p, v) do { \ if ((err = aacEncoder_SetParam(enc, (p), (v))) != AACENC_OK) { fail(#p, err); goto done; } \ } while (0) SET(AACENC_AOT, aot); SET(AACENC_SAMPLERATE, sample_rate); SET(AACENC_CHANNELMODE, channels == 2 ? MODE_2 : MODE_1); SET(AACENC_CHANNELORDER, 1); /* WAV channel order */ SET(AACENC_TRANSMUX, TT_MP4_ADTS); SET(AACENC_AFTERBURNER, 1); /* better quality, a little slower */ if (vbr > 0) { SET(AACENC_BITRATEMODE, vbr); } else { SET(AACENC_BITRATEMODE, 0); SET(AACENC_BITRATE, bitrate); } #undef SET if ((err = aacEncEncode(enc, NULL, NULL, NULL, NULL)) != AACENC_OK) { fail("encoder init", err); goto done; } if ((err = aacEncInfo(enc, &info)) != AACENC_OK) { fail("aacEncInfo", err); goto done; } { const int in_samples_max = info.frameLength * channels; unsigned char *frame_buf = malloc(info.maxOutBufBytes); int pos = 0; /* in samples, not frames */ const int total_samples = frames * channels; if (!frame_buf) { snprintf(g_err, sizeof(g_err), "out of memory"); goto done; } for (;;) { AACENC_BufDesc in_desc = {0}, out_desc = {0}; AACENC_InArgs in_args = {0}; AACENC_OutArgs out_args = {0}; void *in_ptr, *out_ptr = frame_buf; int in_id = IN_AUDIO_DATA, out_id = OUT_BITSTREAM_DATA; int in_size, out_size = info.maxOutBufBytes; int in_elem = sizeof(short), out_elem = 1; int left = total_samples - pos; int take = left > in_samples_max ? in_samples_max : left; if (take > 0) { in_ptr = (void *)(pcm + pos); in_size = take * (int)sizeof(short); in_args.numInSamples = take; } else { /* Flush: -1 tells the encoder to drain its lookahead and SBR delay. */ in_ptr = NULL; in_size = 0; in_args.numInSamples = -1; } in_desc.numBufs = 1; in_desc.bufs = &in_ptr; in_desc.bufferIdentifiers = &in_id; in_desc.bufSizes = &in_size; in_desc.bufElSizes = &in_elem; out_desc.numBufs = 1; out_desc.bufs = &out_ptr; out_desc.bufferIdentifiers = &out_id; out_desc.bufSizes = &out_size; out_desc.bufElSizes = &out_elem; err = aacEncEncode(enc, &in_desc, &out_desc, &in_args, &out_args); if (err == AACENC_ENCODE_EOF) break; if (err != AACENC_OK) { fail("aacEncEncode", err); free(frame_buf); goto done; } if (out_args.numOutBytes > 0 && !push(frame_buf, out_args.numOutBytes, &cap)) { free(frame_buf); goto done; } pos += out_args.numInSamples; if (take <= 0 && out_args.numOutBytes == 0) break; } free(frame_buf); } if (g_out_len == 0) { snprintf(g_err, sizeof(g_err), "encoder produced no output"); goto done; } ok = 1; done: if (enc) aacEncClose(&enc); if (!ok) fdk_release(); return ok; } /* * Ask the encoder whether a combination is usable before offering it, so the UI * never shows a preset that would fail. Same parameters as fdk_encode. */ EMSCRIPTEN_KEEPALIVE int fdk_supports(int sample_rate, int channels, int aot, int bitrate, int vbr) { HANDLE_AACENCODER enc = NULL; AACENC_ERROR err; int ok = 0; if (channels < 1 || channels > 2) return 0; if (aot == 29 && channels != 2) return 0; if (aacEncOpen(&enc, 0, channels) != AACENC_OK) return 0; if (aacEncoder_SetParam(enc, AACENC_AOT, aot) != AACENC_OK) goto out; if (aacEncoder_SetParam(enc, AACENC_SAMPLERATE, sample_rate) != AACENC_OK) goto out; if (aacEncoder_SetParam(enc, AACENC_CHANNELMODE, channels == 2 ? MODE_2 : MODE_1) != AACENC_OK) goto out; if (aacEncoder_SetParam(enc, AACENC_TRANSMUX, TT_MP4_ADTS) != AACENC_OK) goto out; if (vbr > 0) { if (aacEncoder_SetParam(enc, AACENC_BITRATEMODE, vbr) != AACENC_OK) goto out; } else { if (aacEncoder_SetParam(enc, AACENC_BITRATEMODE, 0) != AACENC_OK) goto out; if (aacEncoder_SetParam(enc, AACENC_BITRATE, bitrate) != AACENC_OK) goto out; } err = aacEncEncode(enc, NULL, NULL, NULL, NULL); ok = (err == AACENC_OK); out: if (enc) aacEncClose(&enc); return ok; }