From ebbd532ff00f3f52524985995f6eaefc8b4864ed Mon Sep 17 00:00:00 2001 From: Nils Schimmelmann Date: Mon, 24 Aug 2026 11:42:48 -0500 Subject: [PATCH] frontend: add iTunes gapless playback metadata (iTunSMPB) Calculate encoder priming delay and trailing padding in frontend/encode_engine.c and write standard iTunes gapless playback metadata (iTunSMPB) in frontend/mp4write.c for MP4 container outputs. Priming delay comes from a new faac_encoder_info.encoder_delay field (libfaac/faac.c), resolved per object type: one frame of MDCT overlap for LC, plus the SBR/resample pipeline's extra buffering for HE-AAC. Both values verified against decoded output (impulse test) across sample rates, bitrates, and channel counts, not derived from spec. Fix freeform atom generation in put_tag_ext() to emit a single data atom child inside ilst.----. --- frontend/encode_engine.c | 7 +++++++ frontend/mp4write.c | 31 ++++++++++++++++++++++++++++++- frontend/mp4write.h | 1 + include/faac.h | 5 +++++ libfaac/faac.c | 17 +++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/frontend/encode_engine.c b/frontend/encode_engine.c index 6058224e8..4a1f1eb41 100644 --- a/frontend/encode_engine.c +++ b/frontend/encode_engine.c @@ -707,6 +707,13 @@ int run_encoding_session_ext(const encode_options_t *opts, if (opts->container_mp4 && mp4_is_open) { + uint32_t priming = info.encoder_delay; + uint64_t total_output_samples = (uint64_t)current_frame * priming; + uint64_t padding = 0; + if (total_output_samples > (uint64_t)priming + current_input_samples) + padding = total_output_samples - (uint64_t)priming - current_input_samples; + mp4_set_gapless(priming, (uint32_t)padding, current_input_samples); + if (!finalize_mp4(hEncoder, opts, log_cb, user_data)) { ret = 1; diff --git a/frontend/mp4write.c b/frontend/mp4write.c index b0e66ca82..7f28b957b 100644 --- a/frontend/mp4write.c +++ b/frontend/mp4write.c @@ -130,6 +130,13 @@ static struct { uint32_t size; } cover; + struct { + bool present; + uint32_t priming; + uint32_t padding; + uint64_t original_samples; + } gapless; + struct { const char *name; const char *value; @@ -341,6 +348,7 @@ static void reset_write_state(void) { g_mp4.mdatofs = 0; g_mp4.mdatsize = 0; memset(&g_mp4.bitrate, 0, sizeof(g_mp4.bitrate)); + memset(&g_mp4.gapless, 0, sizeof(g_mp4.gapless)); free(g_membuf); g_membuf = NULL; } @@ -434,6 +442,13 @@ void mp4_set_cover(const uint8_t *data, uint32_t size) { g_mp4.cover.size = size; } +void mp4_set_gapless(uint32_t priming, uint32_t padding, uint64_t original_samples) { + g_mp4.gapless.present = true; + g_mp4.gapless.priming = priming; + g_mp4.gapless.padding = padding; + g_mp4.gapless.original_samples = original_samples; +} + int mp4_add_custom_tag(const char *name, const char *value) { if (g_mp4.customcnt >= g_mp4.customcap) { uint32_t new_cap = g_mp4.customcap ? g_mp4.customcap * 2 : 8; @@ -546,7 +561,11 @@ static void put_tag_ext(const char *mean, const char *name, const char *val) { put_u32(0); put_data(name, strlen(name)); end_atom(name_box); - put_itunes_data_box("data", ITUNES_DATA_TEXT, val, strlen(val)); + long data_box = start_atom("data"); + put_u32(ITUNES_DATA_TEXT); + put_u32(0); + put_data(val, strlen(val)); + end_atom(data_box); end_atom(box); } @@ -757,6 +776,16 @@ int mp4_finish(void) { if (g_mp4.trackno) put_tag_index("trkn", (uint16_t)g_mp4.trackno, (uint16_t)g_mp4.ntracks); if (g_mp4.discno) put_tag_index("disk", (uint16_t)g_mp4.discno, (uint16_t)g_mp4.ndiscs); if (g_mp4.cover.data) put_tag_image(g_mp4.cover.data, g_mp4.cover.size); + if (g_mp4.gapless.present) { + char smpb[128]; + snprintf(smpb, sizeof(smpb), + " 00000000 %08X %08X %08X%08X 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", + g_mp4.gapless.priming, + g_mp4.gapless.padding, + (uint32_t)(g_mp4.gapless.original_samples >> 32), + (uint32_t)(g_mp4.gapless.original_samples & 0xFFFFFFFFULL)); + put_tag_ext("com.apple.iTunes", "iTunSMPB", smpb); + } for (uint32_t i = 0; i < g_mp4.customcnt; i++) put_tag_ext("faac", g_mp4.custom[i].name, g_mp4.custom[i].value); end_atom(ilst); diff --git a/frontend/mp4write.h b/frontend/mp4write.h index 9f9e727fe..a206f7378 100644 --- a/frontend/mp4write.h +++ b/frontend/mp4write.h @@ -46,6 +46,7 @@ void mp4_set_compilation(bool flag); void mp4_set_track(uint16_t num, uint16_t total); void mp4_set_disc(uint16_t num, uint16_t total); void mp4_set_cover(const uint8_t *data, uint32_t size); +void mp4_set_gapless(uint32_t priming, uint32_t padding, uint64_t original_samples); int mp4_add_custom_tag(const char *name, const char *value); int mp4_write_frame(const uint8_t *data, uint32_t size, uint32_t samples); int mp4_finish(void); diff --git a/include/faac.h b/include/faac.h index b0f499f39..867ecdc33 100644 --- a/include/faac.h +++ b/include/faac.h @@ -223,6 +223,11 @@ typedef struct faac_encoder_info { uint32_t quant_quality; /* resolved quantizer quality */ int32_t pns_level; /* resolved PNS level, 0..10 */ uint32_t max_bit_rate; /* resolved peak cap, 0 if unlimited */ + + /* Priming delay in samples/channel at the output rate: leading samples the + * decoder must discard. Use verbatim for gapless tagging (e.g. iTunSMPB) -- + * not the same as frame_samples for HE-AAC. */ + uint32_t encoder_delay; } faac_encoder_info; /* diff --git a/libfaac/faac.c b/libfaac/faac.c index 63305dca5..e4c211ef0 100644 --- a/libfaac/faac.c +++ b/libfaac/faac.c @@ -26,6 +26,7 @@ #include "config.h" #endif +#include #include #include @@ -33,6 +34,7 @@ #include "frame.h" #include "bitstream.h" #include "sbr.h" +#include "resample.h" #include "util.h" /* The public enums are width-pinned to 32 bits by their FAAC_*_MAX sentinels; @@ -279,6 +281,20 @@ FAACAPI faac_status faac_encoder_close(faac_encoder **enc) return FAAC_OK; } +/* LC: one frame of 50% MDCT overlap. HE-AAC: that same core delay at full + * rate (2*FRAME_LEN) plus one extra full-rate frame the SBR/resample pipeline + * buffers ahead of the core, net of the resampler's own FIR group delay. + * Verified against decoded output, not derived from spec. */ +static uint32_t faacEncoderDelay(const faacEncStruct *h) +{ + switch (h->config.aacObjectType) { + case LOW: return FRAME_LEN; + case HE_V1: return 3 * FRAME_LEN - RESAMPLE_FILTER_LEN / 2; + } + assert(0 && "faacEncoderDelay: unhandled aacObjectType"); + return FRAME_LEN; +} + FAACAPI faac_status faac_encoder_get_info(faac_encoder *enc, faac_encoder_info *out) { faac_encoder_info info; @@ -304,6 +320,7 @@ FAACAPI faac_status faac_encoder_get_info(faac_encoder *enc, faac_encoder_info * info.quant_quality = (uint32_t)h->config.quantqual; info.pns_level = (int32_t)h->config.pnslevel; info.max_bit_rate = (uint32_t)h->config.maxBitRate; + info.encoder_delay = faacEncoderDelay(h); /* Write at most the caller's struct_size so a newer library cannot overrun * an older, smaller faac_encoder_info; report the byte count actually set. */