From b8ba2a1a264b9662d2e54819c37d1526b2addc04 Mon Sep 17 00:00:00 2001 From: Nils Schimmelmann Date: Sun, 30 Aug 2026 07:32:22 -0500 Subject: [PATCH] implement adaptive bit reservoir and PE-driven rate control Introduces an adaptive bit reservoir and Perceptual Entropy (PE) driven rate control architecture to improve audio quality on transient-heavy content and optimize bitrate accuracy across ABR modes. Key changes: - Add fast subblock energy PE calculation (`PsyCalcPE`) to detect transient frames without transcendental math overhead. - Maintain persistent bit reservoir state (`bitReservoir`) capped according to ISO/IEC 14496-3 payload limits. - Implement PE-gated reservoir draws for high-entropy frames and bit replenishment for simple frames. - Stabilize quality feedback using proportional reservoir error correction, dynamic damping, and slew-rate clamping [0.80, 1.20]. - Refine initial quality seeding with sample-rate aware scaling for rapid ABR convergence on short audio clips. --- libfaac/blockswitch.c | 29 +++++++++++-- libfaac/blockswitch.h | 9 ++++ libfaac/frame.c | 95 +++++++++++++++++++++++++++++++++++++------ libfaac/frame.h | 4 ++ 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/libfaac/blockswitch.c b/libfaac/blockswitch.c index 2e7afa19b..c8ad1374e 100644 --- a/libfaac/blockswitch.c +++ b/libfaac/blockswitch.c @@ -151,6 +151,26 @@ void PsyEnd(PsyInfo * psyInfo, unsigned int numChannels) } } +/* Do psychoacoustical analysis */ +/* Fast energy-based Perceptual Entropy approximation: sum subblock high-pass energies + pre-computed in PsyBufferUpdate(), scaling by PE_ENERGY_SCALE to match PE complexity threshold. */ +static void PsyCalcPE(PsyInfo * psyInfo) +{ + psydata_t *psydata = (psydata_t *)psyInfo->data; + if (!psydata) { psyInfo->pe = 0.0f; return; } + float pe = (float)psydata->eng[ENG_WIN_CUR + 0] + (float)psydata->eng[ENG_WIN_CUR + 1] + + (float)psydata->eng[ENG_WIN_CUR + 2] + (float)psydata->eng[ENG_WIN_CUR + 3] + + (float)psydata->eng[ENG_WIN_CUR + 4] + (float)psydata->eng[ENG_WIN_CUR + 5] + + (float)psydata->eng[ENG_WIN_CUR + 6] + (float)psydata->eng[ENG_WIN_CUR + 7]; + psyInfo->pe = pe * PE_ENERGY_SCALE; +} + +static void PsyAnalyzeChannel(PsyInfo * psyInfo) +{ + PsyCheckShort(psyInfo); + PsyCalcPE(psyInfo); +} + /* Do psychoacoustical analysis */ void PsyCalculate(AACElement * elements, int numElements, PsyInfo * psyInfo, unsigned int numChannels @@ -158,7 +178,7 @@ void PsyCalculate(AACElement * elements, int numElements, PsyInfo * psyInfo, { if (elements == NULL) { for (unsigned int channel = 0; channel < numChannels; channel++) - PsyCheckShort(&psyInfo[channel]); + PsyAnalyzeChannel(&psyInfo[channel]); return; } @@ -167,14 +187,15 @@ void PsyCalculate(AACElement * elements, int numElements, PsyInfo * psyInfo, AACElement *elem = &elements[e]; switch (elem->type) { case ID_SCE: - PsyCheckShort(&psyInfo[elem->channels[0]]); + PsyAnalyzeChannel(&psyInfo[elem->channels[0]]); break; case ID_CPE: - PsyCheckShort(&psyInfo[elem->channels[0]]); - PsyCheckShort(&psyInfo[elem->channels[1]]); + PsyAnalyzeChannel(&psyInfo[elem->channels[0]]); + PsyAnalyzeChannel(&psyInfo[elem->channels[1]]); break; case ID_LFE: psyInfo[elem->channels[0]].block_type = ONLY_LONG_WINDOW; + psyInfo[elem->channels[0]].pe = 0.0f; break; default: break; diff --git a/libfaac/blockswitch.h b/libfaac/blockswitch.h index 2e8622f67..6fd8273ad 100644 --- a/libfaac/blockswitch.h +++ b/libfaac/blockswitch.h @@ -26,11 +26,20 @@ extern "C" { struct faacEncStruct; +/* Scaling factor to normalize subblock high-pass energy sums e_w = sum(d[n]^2) relative to full-scale PCM power. + * With PCM float range [-32768, 32767] and 256-sample subblocks, peak subblock energy e_max = 256 * 65536^2 = 1.1e12. + * Scaling by 1.0e-8f normalizes subblock energies so stream PE (totalPE) maps cleanly to the PE_THRESH_PER_CH complexity threshold. */ +#define PE_ENERGY_SCALE (1.0e-8f) + +/* Per-channel Perceptual Entropy complexity threshold: streams exceeding 10.0f/ch PE are classified as high-complexity/transient */ +#define PE_THRESH_PER_CH (10.0f) + typedef struct { int size; int sizeS; int block_type; + float pe; void *data; } PsyInfo; diff --git a/libfaac/frame.c b/libfaac/frame.c index ec0bd226a..f6ff04ac9 100644 --- a/libfaac/frame.c +++ b/libfaac/frame.c @@ -17,6 +17,7 @@ #include #include #include +#include #include "frame.h" #include "coder.h" @@ -41,7 +42,6 @@ #endif /* Rate control tuning constants */ -#define RC_DEADBAND_THRESHOLD 0.05f /* +/- 5% deadband */ #define RC_DAMPING_FACTOR 0.6f /* Control loop damping */ /* Bounds on the peak limiter's quality scale factor: the ceiling guarantees @@ -265,7 +265,24 @@ int faacEncApplyConfig(faacEncStruct* hEncoder, if (!config->quantqual) { - config->quantqual = (float)config->bitRate * hEncoder->numChannels / 1280; + /* Scale initial quality seed by sample-rate frame duration factor (44100 / sampleRate) + * so low sampling rates (e.g. 16 kHz) start at appropriate quality scale factors for + * fast rate-control convergence on short audio clips. */ + float rateFactor = 44100.0f / (float)hEncoder->sampleRate; + /* Precise target-bitrate quality seeding curve: maps bitRate to optimal initial quantqual + * for rapid rate-control convergence without early overshoot or undershoot. */ + float bps = (float)config->bitRate; + float q_seed; + if (bps <= 16000.0f) { + q_seed = 10.0f + 22.0f * (bps / 16000.0f); + } else if (bps <= 64000.0f) { + q_seed = 32.0f + 68.0f * ((bps - 16000.0f) / 48000.0f); + } else { + q_seed = bps / 640.0f; + } + /* Boost initial seed for mono speech streams */ + if (hEncoder->numChannels == 1 && bps >= 32000.0f) q_seed *= 2.5f; + config->quantqual = q_seed * (float)hEncoder->numChannels * rateFactor; if (config->quantqual > DEFQUAL) config->quantqual = (config->quantqual - DEFQUAL) * 3.0f + DEFQUAL; } @@ -368,6 +385,17 @@ int faacEncApplyConfig(faacEncStruct* hEncoder, InitElements(hEncoder->elements, &hEncoder->numElements, (int)hEncoder->numChannels, hEncoder->config.useLfe); RefreshLfeMap(hEncoder); + /* Initialize adaptive bit reservoir for ABR mode */ + if (hEncoder->config.bitRate > 0) { + int desbits = (int)((unsigned long long)hEncoder->numChannels * hEncoder->config.bitRate * FRAME_LEN / hEncoder->sampleRate); + int maxReservoirBits = (int)max(0, (int)(AAC_MAX_BITS_PER_CH * hEncoder->numChannels) - desbits); + hEncoder->bitReservoirCap = min(maxReservoirBits, 2 * desbits); + hEncoder->bitReservoir = hEncoder->bitReservoirCap / 2; + } else { + hEncoder->bitReservoirCap = 0; + hEncoder->bitReservoir = 0; + } + return 1; } @@ -933,23 +961,66 @@ int faacEncEncode(faacEncHandle hpEncoder, * controller doesn't starve the core to pay for SBR. */ sbrBits = SbrContextGetBits(hEncoder->sbrContext, NULL, (int)numChannels, (int)hEncoder->config.aacObjectType, 0); - if (totalBits > sbrBits) - fix = (float)(desbits - sbrBits) / (float)(totalBits - sbrBits); - else - fix = 1.0f; + /* Compute total stream Perceptual Entropy (PE) across channels */ + float totalPE = 0.0f; + for (channel = 0; channel < numChannels; channel++) { + totalPE += hEncoder->psyInfo[channel].pe; + } - if (fix < (1.0f - RC_DEADBAND_THRESHOLD)) { - fix += RC_DEADBAND_THRESHOLD; - } else if (fix > (1.0f + RC_DEADBAND_THRESHOLD)) { - fix -= RC_DEADBAND_THRESHOLD; + /* Update adaptive bit reservoir balance and compute effective frame bits for rate control */ + int effectiveBits = totalBits; + int diff = desbits - totalBits; + + if (diff < 0) { + int excess = -diff; + /* Adaptive burst draw ceiling: 0.5 * desbits for low bitrates (<=48k stereo / <=24k mono), 1.0 * desbits for high bitrates */ + int drawLimit = (hEncoder->config.bitRate <= 24000) ? (desbits / 2) : desbits; + int maxDraw = (excess < drawLimit) ? excess : drawLimit; + /* Data-driven PE complexity threshold: PE_THRESH_PER_CH per channel naturally captures high-entropy transients. + * Bypassing low-entropy frames prevents quality scale-factor inflation and overshoot. */ + if (totalPE > (PE_THRESH_PER_CH * (float)numChannels) && hEncoder->bitReservoir > 0) { + int absorbed = (maxDraw < hEncoder->bitReservoir) ? maxDraw : hEncoder->bitReservoir; + effectiveBits = totalBits - absorbed; + hEncoder->bitReservoir -= absorbed; + } else { + hEncoder->bitReservoir += diff; + if (hEncoder->bitReservoir < 0) hEncoder->bitReservoir = 0; + } } else { + /* Simple frames replenish the reservoir without penalizing feedback rate control */ + int space = hEncoder->bitReservoirCap - hEncoder->bitReservoir; + int deposited = (diff < space) ? diff : space; + hEncoder->bitReservoir += deposited; + effectiveBits = totalBits; + } + + if (effectiveBits > sbrBits) + fix = (float)(desbits - sbrBits) / (float)(effectiveBits - sbrBits); + else fix = 1.0f; + + /* Apply adaptive damping: accelerate rate control recovery when reservoir is depleted or full */ + float damping = RC_DAMPING_FACTOR; + if (hEncoder->bitReservoirCap > 0) { + float fillRatio = (float)hEncoder->bitReservoir / (float)hEncoder->bitReservoirCap; + if (fillRatio < 0.25f || fillRatio > 0.75f) + damping = 0.85f; + + /* Additive reservoir proportional correction to eliminate long-term drift */ + float resErr = fillRatio - 0.5f; + /* Adaptive gain adjustment for HE-AAC to compensate for fixed SBR payload bit offset */ + float kp = (hEncoder->config.aacObjectType == HE_V1 && hEncoder->config.bitRate <= 32000) ? 0.12f : 0.08f; + fix += kp * resErr; } /* Apply damping to the quality adjustment */ - fix = (fix - 1.0f) * RC_DAMPING_FACTOR + 1.0f; + fix = (fix - 1.0f) * damping + 1.0f; - hEncoder->aacquantCfg.quality *= fix; + /* Skip small adjustments (< 0.5%) to reduce quality scale update math and keep quality steady */ + if (fabsf(fix - 1.0f) > 0.005f) { + fix = (fix < 0.80f) ? 0.80f : ((fix > 1.20f) ? 1.20f : fix); + hEncoder->aacquantCfg.quality *= fix; + } if (hEncoder->aacquantCfg.quality > maxqual) hEncoder->aacquantCfg.quality = maxqual; diff --git a/libfaac/frame.h b/libfaac/frame.h index f76b5a0d2..cf4cb9d90 100644 --- a/libfaac/frame.h +++ b/libfaac/frame.h @@ -112,6 +112,10 @@ typedef struct faacEncStruct { /* Peak-limiter retry scratch: one buffer per channel holding book[] at * [0] and sf[] at [MAX_SCFAC_BANDS]. */ int *peakSnap[MAX_CHANNELS]; + + /* Adaptive bit reservoir state */ + int bitReservoir; /* current bit reservoir level in bits */ + int bitReservoirCap; /* max bit reservoir capacity in bits */ } faacEncStruct; /* Configuration worker behind faac_encoder_open(): validates the config,