Commit 4984c18829 for ffmpeg

commit 4984c18829206c63a7c17b1995aee68e65f52f1b
Author: Jun Zhao <barryjzhao@tencent.com>
Date:   Fri Oct 9 12:20:37 2026 +0800

    avcodec/amrwbdec: generate comfort noise for SID frames

    A SID frame (frame type 9) returned AVERROR_PATCHWELCOME and was
    dropped, so a DTX stream lost 20 ms per SID frame.

    Decode the 40-bit SID and follow the TS 26.173 DTX state machine
    in floating point: interpolate and mute between updates, then
    synthesize noise at the decoded energy. After a silent period has
    started, frames with no data keep producing that noise; until
    comfort-noise parameters exist, the frame stays silent. Unused
    frame types between SID and NO_DATA are treated as NO_DATA.

    Duration matches the reference decoder. Speech samples are
    unchanged. FATE checks the frame count and the level.

    Signed-off-by: Jun Zhao <barryjzhao@tencent.com>

diff --git a/Makefile b/Makefile
index 93dc74f721..9eaeb27b7a 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ vpath %.ptx  $(SRC_PATH)
 vpath %.metal $(SRC_PATH)
 vpath %/fate_config.sh.template $(SRC_PATH)

-TESTTOOLS   = audiogen videogen rotozoom tiny_psnr tiny_ssim base64 audiomatch
+TESTTOOLS   = audiogen videogen rotozoom tiny_psnr tiny_ssim base64 audiomatch amrgen
 HOSTPROGS  := $(TESTTOOLS:%=tests/%) doc/print_options

 ALLFFLIBS =            \
diff --git a/libavcodec/amrwbdec.c b/libavcodec/amrwbdec.c
index 3557b4709d..f4daee37df 100644
--- a/libavcodec/amrwbdec.c
+++ b/libavcodec/amrwbdec.c
@@ -32,6 +32,7 @@
 #include "libavutil/lfg.h"

 #include "avcodec.h"
+#include "get_bits.h"
 #include "lsp.h"
 #include "celp_filters.h"
 #include "celp_math.h"
@@ -49,6 +50,35 @@
 #include "mips/amrwbdec_mips.h"
 #endif /* ARCH_MIPS */

+/**
+ * High band gain of the comfort noise relative to the low band one.
+ *
+ * This is a non-reference approximation for the high band excitation
+ * scaling, not a value ported from the reference decoder: the reference
+ * shapes much less noise into the high band but has no single constant
+ * counterpart to this figure.
+ */
+#define CN_HF_GAIN_FAC  0.25f
+
+/** Received frame types used by the comfort noise (DTX) state machine */
+enum RXFrameType {
+    RX_SPEECH_GOOD = 0,
+    RX_SPEECH_PROBABLY_DEGRADED,
+    RX_SPEECH_LOST,
+    RX_SPEECH_BAD,
+    RX_SID_FIRST,
+    RX_SID_UPDATE,
+    RX_SID_BAD,
+    RX_NO_DATA
+};
+
+/** States of the comfort noise (DTX) state machine */
+enum DTXState {
+    DTX_SPEECH = 0,
+    DTX,
+    DTX_MUTE
+};
+
 typedef struct AMRWBContext {
     AMRWBFrame                             frame; ///< AMRWB parameters decoded from bitstream
     enum Mode                        fr_cur_mode; ///< mode index of current frame
@@ -96,6 +126,28 @@ typedef struct AMRWBContext {
     CELPFContext                       celpf_ctx; ///< context for filters for CELP-based codecs
     CELPMContext                       celpm_ctx; ///< context for fixed point math operations

+    /* Comfort noise (DTX) state */
+    int   dtx_state;                              ///< current DTX state (SPEECH, DTX or DTX_MUTE)
+    int   since_last_sid;                         ///< frames since the last SID update
+    float sid_period_inv;                         ///< inverse of the SID update period
+    float cn_log_en;                              ///< comfort noise log2 energy of the last SID
+    float cn_log_en_old;                          ///< comfort noise log2 energy before the last SID
+    float cn_isf[LP_ORDER];                       ///< comfort noise ISF vector of the last SID
+    float cn_isf_old[LP_ORDER];                   ///< comfort noise ISF vector before the last SID
+    int16_t cn_seed;                              ///< comfort noise generator seed
+    int16_t cn_dither_seed;                       ///< comfort noise dithering seed
+    int   cn_dith;                                ///< comfort noise dithering flag from the last SID
+    float isf_hist[DTX_HIST_SIZE][LP_ORDER];      ///< ISF history used when a SID frame starts DTX
+    float log_en_hist[DTX_HIST_SIZE];             ///< energy history used when a SID frame starts DTX
+    int   hist_ptr;                               ///< next position to write in the DTX history
+    int   hangover_count;                         ///< remaining DTX hangover frames
+    int   dec_ana_elapsed;                        ///< frames since the last DTX hangover
+    int   hangover_added;                         ///< a DTX hangover was assumed for this frame
+    int   sid_frame;                              ///< the current frame carries CN parameters
+    int   valid_data;                             ///< the CN parameters of this frame may be used
+    int   data_updated;                           ///< CN parameters have been renewed at least once
+    int   vad_hist;                               ///< consecutive frames without voice activity
+
 } AMRWBContext;

 typedef struct AMRWBChannelsContext {
@@ -134,6 +186,24 @@ static av_cold int amrwb_decode_init(AVCodecContext *avctx)
         for (i = 0; i < 4; i++)
             ctx->prediction_error[i] = MIN_ENERGY;

+        /* Comfort noise state, matches the DTX decoder reset */
+        ctx->dtx_state       = DTX_SPEECH;
+        ctx->sid_period_inv  = 0.25f;
+        ctx->cn_log_en       = CN_LOG_EN_INIT;
+        ctx->cn_log_en_old   = CN_LOG_EN_INIT;
+        ctx->cn_seed         = CN_NOISE_SEED_INIT;
+        ctx->cn_dither_seed  = CN_NOISE_SEED_INIT;
+        ctx->hangover_count  = DTX_HANG_CONST;
+        ctx->dec_ana_elapsed = 32767;
+
+        for (i = 0; i < LP_ORDER; i++)
+            ctx->cn_isf[i] = ctx->cn_isf_old[i] = isf_init[i] * (1.0f / (1 << 15));
+
+        for (i = 0; i < DTX_HIST_SIZE; i++) {
+            memcpy(ctx->isf_hist[i], ctx->cn_isf, LP_ORDER * sizeof(*ctx->cn_isf));
+            ctx->log_en_hist[i] = CN_LOG_EN_HIST_INIT;
+        }
+
         ff_acelp_filter_init(&ctx->acelpf_ctx);
         ff_acelp_vectors_init(&ctx->acelpv_ctx);
         ff_celp_filter_init(&ctx->celpf_ctx);
@@ -556,8 +626,8 @@ static void decode_fixed_vector(float *fixed_vector, const uint16_t *pulse_hi,
                            ((int) pulse_hi[i] << 11), 4, 1);
         break;
     default:
-        av_unreachable("Everything >= MODE_SID is impossible: MODE_SID is patchwelcome,"
-                       "> MODE_SID is invalid");
+        av_unreachable("SID and the unused frame types are handled before this "
+                       "function, so only speech modes reach it");
     }

     memset(fixed_vector, 0, sizeof(float) * AMRWB_SFR_SIZE);
@@ -1109,6 +1179,474 @@ static void update_sub_state(AMRWBContext *ctx)
             LP_ORDER_16k * sizeof(float));
 }

+/**
+ * Comfort noise (DTX) generation, see 3GPP TS 26.192.
+ *
+ * The implementation follows the reference decoder in 3GPP TS 26.173,
+ * with the fixed-point arithmetic replaced by floating point. ISF vectors
+ * and log2 energies stay in the same units as the speech path (0..0.5
+ * normalized frequency and log2 of the excitation energy). The tables are
+ * the noise ISF codebooks from that specification.
+ */
+
+/**
+ * Generate the next sample of the white noise used as comfort noise
+ * excitation. The seed wraps around like a 16 bit integer.
+ */
+static int16_t noise_gen_amrwb(int16_t *seed)
+{
+    *seed = (int16_t)(uint16_t)((int32_t) *seed * 31821 + 13849);
+
+    return *seed;
+}
+
+/**
+ * Decode the comfort noise parameters of a SID frame.
+ *
+ * The payload is 40 bits, of which 36 are used: five ISF indices
+ * (6, 6, 6, 5 and 5 bits), the log energy index (6 bits), the comfort noise
+ * dithering flag and a flag telling whether this is the first SID frame of a
+ * silent period.
+ *
+ * @param[in]     buf               Pointer to the frame payload (5 bytes)
+ * @param[out]    ind               The five ISF indices
+ * @param[out]    en_idx            Index of the logarithmic frame energy
+ *
+ * @return non zero if this is a SID update frame
+ */
+static int decode_sid_frame(AMRWBContext *ctx, const uint8_t *buf,
+                            int *ind, int *en_idx)
+{
+    GetBitContext gb;
+    int dith, sid_update;
+
+    init_get_bits8(&gb, buf, 5);
+
+    ind[0] = get_bits(&gb, 6);
+    ind[1] = get_bits(&gb, 6);
+    ind[2] = get_bits(&gb, 6);
+    ind[3] = get_bits(&gb, 5);
+    ind[4] = get_bits(&gb, 5);
+    *en_idx = get_bits(&gb, 6);
+
+    /* TS 26.173 reads the dither flag only on a valid SID_UPDATE. A
+       SID_FIRST frame carries the bit but must not change the flag used
+       for later frames that have no data. */
+    dith = get_bits1(&gb);
+    sid_update = get_bits1(&gb);
+    if (sid_update)
+        ctx->cn_dith = dith;
+
+    return sid_update;
+}
+
+/**
+ * Decode the quantized background noise ISF vector of a SID frame.
+ *
+ * @param[in]     ind               The five quantization indices
+ * @param[out]    isf_q             Buffer for isf_q[LP_ORDER]
+ */
+static void decode_isf_noise(const int *ind, float *isf_q)
+{
+    int i;
+
+    isf_q[0] = dico1_isf_noise[ind[0]][0] * (1.0f / (1 << 15));
+    isf_q[1] = dico1_isf_noise[ind[0]][1] * (1.0f / (1 << 15));
+
+    for (i = 0; i < 3; i++) {
+        isf_q[i + 2] = dico2_isf_noise[ind[1]][i] * (1.0f / (1 << 15));
+        isf_q[i + 5] = dico3_isf_noise[ind[2]][i] * (1.0f / (1 << 15));
+    }
+
+    for (i = 0; i < 4; i++) {
+        isf_q[i +  8] = dico4_isf_noise[ind[3]][i] * (1.0f / (1 << 15));
+        isf_q[i + 12] = dico5_isf_noise[ind[4]][i] * (1.0f / (1 << 15));
+    }
+
+    for (i = 0; i < LP_ORDER; i++)
+        isf_q[i] += mean_isf_noise[i] * (1.0f / (1 << 15));
+
+    ff_set_min_dist_lsf(isf_q, MIN_ISF_SPACING, LP_ORDER - 1);
+}
+
+/**
+ * Draw a dithering value in [-1, 1] from the comfort noise generator.
+ *
+ * @param[in,out] ctx               The Context
+ */
+static float cn_dither_noise(AMRWBContext *ctx)
+{
+    return ((noise_gen_amrwb(&ctx->cn_dither_seed) >> 1) +
+            (noise_gen_amrwb(&ctx->cn_dither_seed) >> 1)) * (1.0f / 32768.0f);
+}
+
+/**
+ * Insert comfort noise dithering, used when the background noise is not
+ * stationary.
+ *
+ * @param[in,out] ctx               The Context
+ * @param[in,out] isf               ISF vector of the comfort noise
+ * @param[in,out] log_en            log2 energy of the comfort noise
+ */
+static void cn_dithering(AMRWBContext *ctx, float *isf, float *log_en)
+{
+    float fac;
+    int i;
+
+    /* Insert comfort noise dithering for energy parameter */
+    *log_en = FFMAX(*log_en + cn_dither_noise(ctx) * CN_DITH_GAIN_FAC, 0.0f);
+
+    /* Insert comfort noise dithering for spectral parameters (ISF vector) */
+    fac = ISF_DITH_FAC_LOW;
+    isf[0] = FFMAX(isf[0] + cn_dither_noise(ctx) * fac, MIN_ISF_SPACING);
+
+    for (i = 1; i < LP_ORDER - 1; i++) {
+        float cur;
+        fac += ISF_DITH_FAC_STEP;
+        cur = isf[i] + cn_dither_noise(ctx) * fac;
+        /* Make sure that the ISF spacing remains at least ISF_DITH_GAP */
+        isf[i] = FFMAX(cur, isf[i - 1] + ISF_DITH_GAP);
+    }
+
+    isf[LP_ORDER - 2] = FFMIN(isf[LP_ORDER - 2], 0.5f);
+}
+
+/**
+ * Store the ISF vector and the excitation energy of a speech frame so
+ * that the first SID frame of a silent period can reuse them.
+ *
+ * @param[in,out] ctx               The Context
+ * @param[in]     isf               ISF vector of the decoded frame
+ * @param[in]     exc_energy        Excitation energy of the decoded frame
+ */
+static void dtx_activity_update(AMRWBContext *ctx, const float *isf,
+                                float exc_energy)
+{
+    ctx->hist_ptr = (ctx->hist_ptr + 1) & (DTX_HIST_SIZE - 1);
+    memcpy(ctx->isf_hist[ctx->hist_ptr], isf, LP_ORDER * sizeof(*isf));
+    ctx->log_en_hist[ctx->hist_ptr] = log2f(FFMAX(exc_energy, 1.0f) /
+                                            (4 * AMRWB_SFR_SIZE));
+}
+
+/**
+ * Update the comfort noise state and generate the excitation of the
+ * comfort noise for this frame.
+ *
+ * @param[in,out] ctx               The Context
+ * @param[out]    cn_exc            Excitation for the whole frame (256 samples)
+ * @param[in]     new_state         DTX state of this frame
+ * @param[in]     ind               ISF indices of the SID frame
+ * @param[in]     en_idx            Energy index of the SID frame
+ */
+static void dtx_dec(AMRWBContext *ctx, float *cn_exc, int new_state,
+                    const int *ind, int en_idx)
+{
+    float *isf = ctx->isf_cur;
+    float log_en, int_fac;
+    int i, j, period;
+
+    /* SID frames that start a silent period reuse the noise parameters
+       of the speech frames preceding them. */
+    if (ctx->hangover_added && ctx->sid_frame) {
+        int ptr = (ctx->hist_ptr + 1) & (DTX_HIST_SIZE - 1);
+
+        memcpy(ctx->isf_hist[ptr], ctx->isf_hist[ctx->hist_ptr],
+               sizeof(ctx->isf_hist[0]));
+        ctx->log_en_hist[ptr] = ctx->log_en_hist[ctx->hist_ptr];
+
+        ctx->cn_log_en = 0.0f;
+        for (i = 0; i < DTX_HIST_SIZE; i++)
+            ctx->cn_log_en += ctx->log_en_hist[i];
+        /* the reference decoder keeps the average in Q9 and adds 2 so that
+           the value stays positive, it is subtracted again when the level
+           of the excitation is computed */
+        ctx->cn_log_en = ctx->cn_log_en / DTX_HIST_SIZE + 2.0f;
+        ctx->cn_log_en = FFMAX(ctx->cn_log_en, 0.0f);
+
+        for (j = 0; j < LP_ORDER; j++) {
+            float acc = 0.0f;
+            for (i = 0; i < DTX_HIST_SIZE; i++)
+                acc += ctx->isf_hist[i][j];
+            ctx->cn_isf[j] = acc / DTX_HIST_SIZE;
+        }
+    }
+
+    if (ctx->sid_frame) {
+        memcpy(ctx->cn_isf_old, ctx->cn_isf, LP_ORDER * sizeof(*ctx->cn_isf));
+        ctx->cn_log_en_old = ctx->cn_log_en;
+
+        if (ctx->valid_data) {
+            period = FFMIN(ctx->since_last_sid, 32);
+            ctx->sid_period_inv = period >= 2 ? 1.0f / period : 0.5f;
+
+            decode_isf_noise(ind, ctx->cn_isf);
+            ctx->cn_log_en = en_idx * (1.0f / 2.625f);
+
+            /* no interpolation at startup or right after speech */
+            if (!ctx->data_updated || ctx->dtx_state == DTX_SPEECH) {
+                memcpy(ctx->cn_isf_old, ctx->cn_isf,
+                       LP_ORDER * sizeof(*ctx->cn_isf));
+                ctx->cn_log_en_old = ctx->cn_log_en;
+            }
+        }
+    }
+
+    /* A frame carrying new parameters ends the interpolation, so the
+       parameters of this very frame are the ones used below. The counter is
+       reset again at the end of this function. */
+    if (ctx->sid_frame && ctx->valid_data)
+        ctx->since_last_sid = 0;
+
+    /* Interpolate the comfort noise parameters since the last update */
+    int_fac = FFMIN(ctx->since_last_sid * ctx->sid_period_inv, 1.0f);
+
+    log_en = int_fac * ctx->cn_log_en + (1.0f - int_fac) * ctx->cn_log_en_old;
+    for (j = 0; j < LP_ORDER; j++)
+        isf[j] = int_fac * ctx->cn_isf[j] + (1.0f - int_fac) * ctx->cn_isf_old[j];
+
+    if (ctx->cn_dith)
+        cn_dithering(ctx, isf, &log_en);
+
+    /* Generate the excitation and scale it to the wanted energy */
+    for (i = 0; i < 4 * AMRWB_SFR_SIZE; i++)
+        cn_exc[i] = noise_gen_amrwb(&ctx->cn_seed) >> 4;
+
+    /* the excitation carries the energy of the decoded noise parameters */
+    ff_scale_vector_to_given_sum_of_squares(cn_exc, cn_exc,
+                                            (4 * AMRWB_SFR_SIZE) *
+                                            exp2f(log_en - 2.0f),
+                                            4 * AMRWB_SFR_SIZE);
+
+    if (new_state == DTX_MUTE) {
+        /* Mute the comfort noise, nothing has been updated for a while */
+        period = ctx->since_last_sid;
+        if (period > 32)
+            period = 32;
+        if (period <= 0)
+            period = 8;
+        ctx->sid_period_inv = 1.0f / period;
+        ctx->since_last_sid = 0;
+        ctx->cn_log_en_old  = ctx->cn_log_en;
+        ctx->cn_log_en     -= CN_LOG_EN_MUTE_STEP;
+    }
+
+    if (ctx->sid_frame &&
+        (ctx->valid_data || (!ctx->valid_data && ctx->hangover_added))) {
+        ctx->since_last_sid = 0;
+        ctx->data_updated   = 1;
+    }
+}
+
+/**
+ * Update the comfort noise state machine for a received frame.
+ *
+ * @param[in,out] ctx               The Context
+ * @param[in]     frame_type        Type of the received frame
+ *
+ * @return the DTX state to decode this frame with
+ */
+static int dtx_handler(AMRWBContext *ctx, int frame_type)
+{
+    int new_state, enc_state;
+
+    if (frame_type == RX_SID_FIRST  || frame_type == RX_SID_UPDATE ||
+        frame_type == RX_SID_BAD    ||
+        ((ctx->dtx_state == DTX || ctx->dtx_state == DTX_MUTE) &&
+         (frame_type == RX_NO_DATA    || frame_type == RX_SPEECH_BAD ||
+          frame_type == RX_SPEECH_LOST))) {
+        new_state = DTX;
+
+        /* stay muted for these input types */
+        if (ctx->dtx_state == DTX_MUTE &&
+            (frame_type == RX_SID_BAD   || frame_type == RX_SID_FIRST  ||
+             frame_type == RX_SPEECH_LOST || frame_type == RX_NO_DATA))
+            new_state = DTX_MUTE;
+
+        ctx->since_last_sid = FFMIN(ctx->since_last_sid + 1, 32767);
+
+        /* SID updates are handled separately to avoid muting a late one */
+        if (frame_type != RX_SID_UPDATE &&
+            ctx->since_last_sid > DTX_MAX_EMPTY_THRESH)
+            new_state = DTX_MUTE;
+    } else {
+        new_state = DTX_SPEECH;
+        ctx->since_last_sid = 0;
+    }
+
+    if (!ctx->data_updated && frame_type == RX_SID_UPDATE)
+        ctx->dec_ana_elapsed = 0;
+
+    ctx->dec_ana_elapsed = FFMIN(ctx->dec_ana_elapsed + 1, 32767);
+    ctx->hangover_added  = 0;
+
+    if (frame_type == RX_SID_FIRST || frame_type == RX_SID_UPDATE ||
+        frame_type == RX_SID_BAD   ||
+        (frame_type == RX_NO_DATA && (ctx->dtx_state != DTX_SPEECH ||
+                                      ctx->vad_hist >= DTX_HANG_CONST)))
+        enc_state = DTX;
+    else
+        enc_state = DTX_SPEECH;
+
+    if (enc_state == DTX_SPEECH) {
+        ctx->hangover_count = DTX_HANG_CONST;
+    } else if (ctx->dec_ana_elapsed > DTX_ELAPSED_FRAMES_THRESH) {
+        ctx->hangover_added  = 1;
+        ctx->dec_ana_elapsed = 0;
+        ctx->hangover_count  = 0;
+    } else if (!ctx->hangover_count) {
+        ctx->dec_ana_elapsed = 0;
+    } else {
+        ctx->hangover_count--;
+    }
+
+    if (new_state != DTX_SPEECH) {
+        ctx->sid_frame  = 0;
+        ctx->valid_data = 0;
+
+        if (frame_type == RX_SID_FIRST) {
+            ctx->sid_frame = 1;
+        } else if (frame_type == RX_SID_UPDATE) {
+            ctx->sid_frame  = 1;
+            ctx->valid_data = 1;
+        } else if (frame_type == RX_SID_BAD) {
+            ctx->sid_frame      = 1;
+            ctx->hangover_added = 0;
+        }
+    }
+
+    return new_state;
+}
+
+/**
+ * Reset the speech decoder state, it is not used during a silent period.
+ */
+static void cn_reset_speech_state(AMRWBContext *ctx)
+{
+    memset(ctx->excitation_buf, 0, sizeof(ctx->excitation_buf));
+    memset(ctx->isf_q_past, 0, sizeof(ctx->isf_q_past));
+
+    ctx->first_frame       = 1;
+    ctx->prev_tr_gain      = 0.0f;
+    ctx->prev_ir_filter_nr = 0;
+    ctx->tilt_coef         = 0.0f;
+
+    /* Reset_decoder(st, 0) clears the phase-dispersion memory, including
+       the previous fixed gain and the pitch-gain history. */
+    memset(ctx->pitch_gain, 0, sizeof(ctx->pitch_gain));
+    memset(ctx->fixed_gain, 0, sizeof(ctx->fixed_gain));
+    ctx->prev_sparse_fixed_gain = 0.0f;
+}
+
+/**
+ * Post-process one synthesized subframe, generate the high frequency band
+ * and store the result at 16 kHz.
+ *
+ * @param[in,out] ctx               The Context
+ * @param[out]    sub_buf           Output samples of the subframe at 16 kHz
+ * @param[in]     synth_exc         Excitation the subframe was built from
+ * @param[in]     sub               Subframe index
+ * @param[in]     hb_idx            High-band gain index (23k85 mode only)
+ * @param[in]     vad               Voice activity flag of the frame
+ * @param[in]     hf_gain           Scaling of the high band noise
+ */
+static void subframe_output(AMRWBContext *ctx, float *sub_buf,
+                            const float *synth_exc, int sub, uint16_t hb_idx,
+                            uint8_t vad, float hf_gain)
+{
+    float hb_exc[AMRWB_SFR_SIZE_16k];
+    float hb_samples[AMRWB_SFR_SIZE_16k];
+    float hb_gain;
+    int i;
+
+    /* Synthesis speech post-processing */
+    de_emphasis(&ctx->samples_up[UPS_MEM_SIZE], &ctx->samples_az[LP_ORDER],
+                PREEMPH_FAC, ctx->demph_mem);
+
+    ctx->acelpf_ctx.acelp_apply_order_2_transfer_function(&ctx->samples_up[UPS_MEM_SIZE],
+                                                          &ctx->samples_up[UPS_MEM_SIZE], hpf_zeros, hpf_31_poles,
+                                                          hpf_31_gain, ctx->hpf_31_mem, AMRWB_SFR_SIZE);
+
+    upsample_5_4(sub_buf, &ctx->samples_up[UPS_FIR_SIZE],
+                 AMRWB_SFR_SIZE_16k, &ctx->celpm_ctx);
+
+    /* High frequency band (6.4 - 7.0 kHz) generation part */
+    ctx->acelpf_ctx.acelp_apply_order_2_transfer_function(hb_samples,
+                                                          &ctx->samples_up[UPS_MEM_SIZE], hpf_zeros, hpf_400_poles,
+                                                          hpf_400_gain, ctx->hpf_400_mem, AMRWB_SFR_SIZE);
+
+    hb_gain = find_hb_gain(ctx, hb_samples, hb_idx, vad);
+
+    scaled_hb_excitation(ctx, hb_exc, synth_exc, hb_gain * hf_gain);
+
+    hb_synthesis(ctx, sub, &ctx->samples_hb[LP_ORDER_16k],
+                 hb_exc, ctx->isf_cur, ctx->isf_past_final);
+
+    /* High-band post-processing filters */
+    hb_fir_filter(hb_samples, bpf_6_7_coef, ctx->bpf_6_7_mem,
+                  &ctx->samples_hb[LP_ORDER_16k]);
+
+    if (ctx->fr_cur_mode == MODE_23k85)
+        hb_fir_filter(hb_samples, lpf_7_coef, ctx->lpf_7_mem,
+                      hb_samples);
+
+    /* Add the low and high frequency bands */
+    for (i = 0; i < AMRWB_SFR_SIZE_16k; i++)
+        sub_buf[i] = (sub_buf[i] + hb_samples[i]) * (1.0f / (1 << 15));
+
+    /* Update buffers and history */
+    update_sub_state(ctx);
+}
+
+/**
+ * Decode one frame of comfort noise into the output buffer.
+ *
+ * @param[in,out] ctx               The Context
+ * @param[in]     new_state         DTX state of this frame
+ * @param[in]     ind               ISF indices of the SID frame
+ * @param[in]     en_idx            Energy index of the SID frame
+ * @param[out]    buf_out           Output buffer for one frame at 16 kHz
+ */
+static void cn_frame(AMRWBContext *ctx, int new_state, const int *ind,
+                     int en_idx, float *buf_out)
+{
+    float cn_exc[4 * AMRWB_SFR_SIZE];
+    float synth_exc[AMRWB_SFR_SIZE];
+    int sub;
+
+    dtx_dec(ctx, cn_exc, new_state, ind, en_idx);
+
+    ff_set_min_dist_lsf(ctx->isf_cur, MIN_ISF_SPACING, LP_ORDER - 1);
+
+    ctx->isf_cur[LP_ORDER - 1] *= 2.0;
+    ff_acelp_lsf2lspd(ctx->isp[3], ctx->isf_cur, LP_ORDER);
+    ff_amrwb_lsp2lpc(ctx->isp[3], ctx->lp_coef[0], LP_ORDER);
+    for (sub = 1; sub < 4; sub++)
+        memcpy(ctx->lp_coef[sub], ctx->lp_coef[0], sizeof(ctx->lp_coef[0]));
+
+    /* the reference decoder uses one set of CN parameters per frame and
+       no adaptive codebook contribution */
+    ctx->pitch_gain[0] = 0.0f;
+
+    for (sub = 0; sub < 4; sub++) {
+        float *sub_buf = buf_out + sub * AMRWB_SFR_SIZE_16k;
+
+        memcpy(ctx->fixed_vector, &cn_exc[sub * AMRWB_SFR_SIZE],
+               AMRWB_SFR_SIZE * sizeof(*ctx->fixed_vector));
+
+        synthesis(ctx, ctx->lp_coef[sub], synth_exc, 1.0f,
+                  ctx->fixed_vector, &ctx->samples_az[LP_ORDER]);
+
+        subframe_output(ctx, sub_buf, synth_exc, sub, 0, !ctx->vad_hist,
+                        CN_HF_GAIN_FAC);
+    }
+
+    memcpy(ctx->isp_sub4_past, ctx->isp[3], LP_ORDER * sizeof(ctx->isp[3][0]));
+    memcpy(ctx->isf_past_final, ctx->isf_cur, LP_ORDER * sizeof(*ctx->isf_cur));
+
+    cn_reset_speech_state(ctx);
+    ctx->dtx_state = new_state;
+}
+
 static int amrwb_decode_frame(AVCodecContext *avctx, AVFrame *frame,
                               int *got_frame_ptr, AVPacket *avpkt)
 {
@@ -1125,16 +1663,16 @@ static int amrwb_decode_frame(AVCodecContext *avctx, AVFrame *frame,
     for (int ch = 0; ch < avctx->ch_layout.nb_channels; ch++) {
         AMRWBContext *ctx  = &s->ch[ch];
         AMRWBFrame   *cf   = &ctx->frame;
-        int expected_fr_size, header_size;
+        int expected_fr_size, header_size, rx_type, new_state;
         float spare_vector[AMRWB_SFR_SIZE];      // extra stack space to hold result from anti-sparseness processing
         float fixed_gain_factor;                 // fixed gain correction factor (gamma)
         float *synth_fixed_vector;               // pointer to the fixed vector that synthesis should use
         float synth_fixed_gain;                  // the fixed gain that synthesis should use
         float voice_fac, stab_fac;               // parameters used for gain smoothing
         float synth_exc[AMRWB_SFR_SIZE];         // post-processed excitation for synthesis
-        float hb_exc[AMRWB_SFR_SIZE_16k];        // excitation for the high frequency band
-        float hb_samples[AMRWB_SFR_SIZE_16k];    // filtered high-band samples from synthesis
-        float hb_gain;
+        int sid_ind[5] = { 0 };                  // ISF indices of a SID frame
+        int sid_en_idx = 0;                      // energy index of a SID frame
+        float frame_exc_energy = 0.0f;           // excitation energy of this frame
         float *buf_out = (float *)frame->extended_data[ch];

         header_size      = decode_mime_header(ctx, buf);
@@ -1143,32 +1681,56 @@ static int amrwb_decode_frame(AVCodecContext *avctx, AVFrame *frame,
         if (!ctx->fr_quality)
             av_log(avctx, AV_LOG_ERROR, "Encountered a bad or corrupted frame\n");

-        if (ctx->fr_cur_mode == NO_DATA || !ctx->fr_quality) {
-            /* The specification suggests a "random signal" and
-               "a muting technique" to "gradually decrease the output level". */
-            av_samples_set_silence(&frame->extended_data[ch], 0, frame->nb_samples, 1, AV_SAMPLE_FMT_FLT);
-            buf += expected_fr_size;
-            buf_size -= expected_fr_size;
-            continue;
-        }
-        if (ctx->fr_cur_mode > MODE_SID) {
-            av_log(avctx, AV_LOG_ERROR,
-                   "Invalid mode %d\n", ctx->fr_cur_mode);
-            return AVERROR_INVALIDDATA;
-        }
-
         if (buf_size < expected_fr_size) {
             av_log(avctx, AV_LOG_ERROR,
                    "Frame too small (%d bytes). Truncated file?\n", buf_size);
             *got_frame_ptr = 0;
             return AVERROR_INVALIDDATA;
         }
-
-        if (ctx->fr_cur_mode == MODE_SID) { /* Comfort noise frame */
-            avpriv_request_sample(avctx, "SID mode");
-            return AVERROR_PATCHWELCOME;
+        /* Classify the received frame for the comfort noise state machine */
+        if (ctx->fr_cur_mode == MODE_SID) {
+            if (ctx->fr_quality) {
+                if (decode_sid_frame(ctx, buf + 1, sid_ind, &sid_en_idx))
+                    rx_type = RX_SID_UPDATE;
+                else
+                    rx_type = RX_SID_FIRST;
+            } else {
+                rx_type = RX_SID_BAD;
+            }
+        } else if (ctx->fr_cur_mode == NO_DATA ||
+                   (ctx->fr_cur_mode > MODE_SID && ctx->fr_cur_mode < SP_LOST)) {
+            /* the reference decoder replaces the unused frame types by
+               frames carrying no data */
+            rx_type = RX_NO_DATA;
+        } else if (ctx->fr_cur_mode == SP_LOST) {
+            rx_type = RX_SPEECH_LOST;
+        } else if (!ctx->fr_quality) {
+            rx_type = RX_SPEECH_BAD;
+        } else {
+            rx_type = RX_SPEECH_GOOD;
+        }
+        new_state = dtx_handler(ctx, rx_type);
+        if (new_state != DTX_SPEECH) {
+            /* Comfort noise is generated for SID frames and, once a silent
+               period has started, for the frames carrying no data at all */
+            cn_frame(ctx, new_state, sid_ind, sid_en_idx,
+                     (float *)frame->extended_data[ch]);
+            buf += expected_fr_size;
+            buf_size -= expected_fr_size;
+            continue;
+        }
+        if (ctx->fr_cur_mode > MODE_SID || !ctx->fr_quality) {
+            /* The specification suggests a "random signal" and
+               "a muting technique" to "gradually decrease the output level".
+               Outside of a silent period no comfort noise parameters are
+               available, so the frames carrying no payload at all are output
+               as silence. They still produce a full frame of samples, keeping
+               the frame count and the timing of the decoded stream correct. */
+            av_samples_set_silence(&frame->extended_data[ch], 0, frame->nb_samples, 1, AV_SAMPLE_FMT_FLT);
+            buf += expected_fr_size;
+            buf_size -= expected_fr_size;
+            continue;
         }
-
         ff_amr_bit_reorder((uint16_t *) &ctx->frame, sizeof(AMRWBFrame),
                            buf + header_size, amr_bit_orderings_by_mode[ctx->fr_cur_mode]);

@@ -1234,6 +1796,11 @@ static int amrwb_decode_frame(AVCodecContext *avctx, AVFrame *frame,
                 ctx->excitation[i] = truncf(ctx->excitation[i]);
             }

+            /* the excitation energy is the comfort noise reference for the
+               silent periods following this frame */
+            frame_exc_energy += ctx->celpm_ctx.dot_productf(ctx->excitation,
+                                                            ctx->excitation,
+                                                            AMRWB_SFR_SIZE);
             /* Post-processing of excitation elements */
             synth_fixed_gain = noise_enhancer(ctx->fixed_gain[0], &ctx->prev_tr_gain,
                                               voice_fac, stab_fac);
@@ -1246,50 +1813,23 @@ static int amrwb_decode_frame(AVCodecContext *avctx, AVFrame *frame,
             synthesis(ctx, ctx->lp_coef[sub], synth_exc, synth_fixed_gain,
                       synth_fixed_vector, &ctx->samples_az[LP_ORDER]);

-            /* Synthesis speech post-processing */
-            de_emphasis(&ctx->samples_up[UPS_MEM_SIZE],
-                        &ctx->samples_az[LP_ORDER], PREEMPH_FAC, ctx->demph_mem);
-
-            ctx->acelpf_ctx.acelp_apply_order_2_transfer_function(&ctx->samples_up[UPS_MEM_SIZE],
-                                                                  &ctx->samples_up[UPS_MEM_SIZE], hpf_zeros, hpf_31_poles,
-                                                                  hpf_31_gain, ctx->hpf_31_mem, AMRWB_SFR_SIZE);
-
-            upsample_5_4(sub_buf, &ctx->samples_up[UPS_FIR_SIZE],
-                         AMRWB_SFR_SIZE_16k, &ctx->celpm_ctx);
-
-            /* High frequency band (6.4 - 7.0 kHz) generation part */
-            ctx->acelpf_ctx.acelp_apply_order_2_transfer_function(hb_samples,
-                                                                  &ctx->samples_up[UPS_MEM_SIZE], hpf_zeros, hpf_400_poles,
-                                                                  hpf_400_gain, ctx->hpf_400_mem, AMRWB_SFR_SIZE);
-
-            hb_gain = find_hb_gain(ctx, hb_samples,
-                                   cur_subframe->hb_gain, cf->vad);
-
-            scaled_hb_excitation(ctx, hb_exc, synth_exc, hb_gain);
-
-            hb_synthesis(ctx, sub, &ctx->samples_hb[LP_ORDER_16k],
-                         hb_exc, ctx->isf_cur, ctx->isf_past_final);
-
-            /* High-band post-processing filters */
-            hb_fir_filter(hb_samples, bpf_6_7_coef, ctx->bpf_6_7_mem,
-                          &ctx->samples_hb[LP_ORDER_16k]);
-
-            if (ctx->fr_cur_mode == MODE_23k85)
-                hb_fir_filter(hb_samples, lpf_7_coef, ctx->lpf_7_mem,
-                              hb_samples);
-
-            /* Add the low and high frequency bands */
-            for (i = 0; i < AMRWB_SFR_SIZE_16k; i++)
-                sub_buf[i] = (sub_buf[i] + hb_samples[i]) * (1.0f / (1 << 15));
-
-            /* Update buffers and history */
-            update_sub_state(ctx);
+            subframe_output(ctx, sub_buf, synth_exc, sub,
+                            cur_subframe->hb_gain, cf->vad, 1.0f);
         }

         /* update state for next frame */
         memcpy(ctx->isp_sub4_past, ctx->isp[3], LP_ORDER * sizeof(ctx->isp[3][0]));
         memcpy(ctx->isf_past_final, ctx->isf_cur, LP_ORDER * sizeof(float));

+        /* keep the noise parameters of this speech frame for the next SID */
+        ctx->isf_cur[LP_ORDER - 1] *= 0.5f;
+        dtx_activity_update(ctx, ctx->isf_cur, frame_exc_energy);
+        ctx->isf_cur[LP_ORDER - 1] *= 2.0f;
+        if (ctx->fr_quality) {
+            ctx->vad_hist = cf->vad ? 0 : FFMIN(ctx->vad_hist + 1, 32767);
+        } else if (ctx->vad_hist > 0)
+            ctx->vad_hist = FFMIN(ctx->vad_hist + 1, 32767);
+        ctx->dtx_state = new_state;
         buf += expected_fr_size;
         buf_size -= expected_fr_size;
     }
diff --git a/tests/Makefile b/tests/Makefile
index 97e9d39339..35db2edb3b 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -65,6 +65,10 @@ tests/data/streamgroups/%: TAG = COPY
 tests/data/streamgroups/%: $(SRC_PATH)/tests/streamgroups/% | tests/data/streamgroups
 	$(M)cp $< $@

+tests/data/fate/%.amr: TAG = GEN
+tests/data/fate/%.amr: tests/amrgen$(HOSTEXESUF) | tests/data/fate
+	$(M)./$< $@
+
 RUNNING_FATE := $(filter check fate%,$(filter-out fate-rsync,$(MAKECMDGOALS)))

 # Check sanity of dependencies when running FATE tests.
diff --git a/tests/amrgen.c b/tests/amrgen.c
new file mode 100644
index 0000000000..855a85aaa6
--- /dev/null
+++ b/tests/amrgen.c
@@ -0,0 +1,298 @@
+/*
+ * Generate the AMR streams used by the DTX FATE tests.
+ *
+ * The storage format is a header plus one table-of-contents byte and the
+ * payload of each frame (RFC 4867). These files are built from that layout
+ * here, the same way audiogen builds its wave files, so the tests do not
+ * need media from the fate-suite.
+ *
+ * Copyright (c) 2026 Jun Zhao
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Narrowband speech is one repeated 4.75 kbit/s frame: pitch lag 40 on the
+   first subframe (coded index 61) and gain index 3. Comfort-noise energy is
+   the 6-bit index carried by a SID_UPDATE. A silent period mutes once more
+   than 50 frames have passed with no SID update. */
+
+enum {
+    NB_FRAME_SAMPLES = 160,
+    WB_FRAME_SAMPLES = 320,
+};
+
+static void die(const char *path)
+{
+    fprintf(stderr, "amrgen: %s: %s\n", path, strerror(errno));
+    exit(1);
+}
+
+static void put_msb(uint8_t *data, int *pos, int n, unsigned value)
+{
+    int i;
+
+    for (i = n - 1; i >= 0; i--) {
+        int bit = (*pos)++;
+
+        if (value & (1u << i))
+            data[bit >> 3] |= 1u << (7 - (bit & 7));
+    }
+}
+
+static void set_lsb_field(uint8_t *data, const uint8_t *bits, int n,
+                          unsigned value)
+{
+    int i;
+
+    for (i = 0; i < n; i++) {
+        if (value & (1u << (n - 1 - i)))
+            data[bits[i] >> 3] |= 1u << (bits[i] & 7);
+    }
+}
+
+static void write_nb_speech(FILE *f)
+{
+    /* Bit positions are those consumed by order_MODE_4k75[], MSB first. */
+    static const uint8_t lag0[]  = { 23, 22, 21, 20, 19, 18, 43, 42 };
+    static const uint8_t gain0[] = { 54, 55, 40, 41, 24, 25, 26, 27 };
+    static const uint8_t gain2[] = { 44, 45, 46, 47, 36, 37, 38, 39 };
+    uint8_t frame[13] = { 0x04 };
+
+    set_lsb_field(frame + 1, lag0, 8, 61);
+    set_lsb_field(frame + 1, gain0, 8, 3);
+    set_lsb_field(frame + 1, gain2, 8, 3);
+    if (fwrite(frame, 1, sizeof(frame), f) != sizeof(frame))
+        die("write");
+}
+
+/* energy is the 6-bit index. update selects SID_UPDATE over SID_FIRST. */
+static void write_nb_sid(FILE *f, int energy, int update)
+{
+    uint8_t frame[6] = { 0x44 };
+    int pos = 0;
+
+    put_msb(frame + 1, &pos, 3, 0);
+    put_msb(frame + 1, &pos, 8, 0);
+    put_msb(frame + 1, &pos, 9, 0);
+    put_msb(frame + 1, &pos, 9, 0);
+    put_msb(frame + 1, &pos, 6, energy);
+    put_msb(frame + 1, &pos, 1, update);
+    put_msb(frame + 1, &pos, 3, 0);
+    if (fwrite(frame, 1, sizeof(frame), f) != sizeof(frame))
+        die("write");
+}
+
+static void write_nodata(FILE *f, int q)
+{
+    uint8_t toc = q ? 0x7c : 0x78;
+
+    if (fwrite(&toc, 1, 1, f) != 1)
+        die("write");
+}
+
+/* Frame types 9..14 are reserved and decoded as NO_DATA. */
+static void write_reserved(FILE *f)
+{
+    uint8_t toc = 0x4c;
+
+    if (fwrite(&toc, 1, 1, f) != 1)
+        die("write");
+}
+
+static void write_wb_sid(FILE *f, int energy)
+{
+    uint8_t frame[6] = { 0x4c };
+    int pos = 0;
+    int i;
+
+    for (i = 0; i < 3; i++)
+        put_msb(frame + 1, &pos, 6, 0);
+    put_msb(frame + 1, &pos, 5, 0);
+    put_msb(frame + 1, &pos, 5, 0);
+    put_msb(frame + 1, &pos, 6, energy);
+    put_msb(frame + 1, &pos, 1, 0); /* dither */
+    put_msb(frame + 1, &pos, 1, 1); /* SID_UPDATE */
+    if (fwrite(frame, 1, sizeof(frame), f) != sizeof(frame))
+        die("write");
+}
+
+static FILE *open_out(const char *path, const char *header)
+{
+    FILE *f = fopen(path, "wb");
+
+    if (!f)
+        die(path);
+    if (fwrite(header, 1, strlen(header), f) != strlen(header))
+        die(path);
+    return f;
+}
+
+/* 8 speech, SID, 4 NO_DATA, SID, 4 NO_DATA, 8 speech.
+   Frame 13 is the second SID. Frame 18 is the first speech after it. */
+static void gen_sid_update(const char *path)
+{
+    FILE *f = open_out(path, "#!AMR\n");
+    int i;
+
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    write_nb_sid(f, 16, 1);
+    for (i = 0; i < 4; i++)
+        write_nodata(f, 1);
+    write_nb_sid(f, 16, 1);
+    for (i = 0; i < 4; i++)
+        write_nodata(f, 1);
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    if (fclose(f))
+        die(path);
+}
+
+/* 8 speech, quiet SID, 4 NO_DATA, loud SID, 4 NO_DATA, 4 speech.
+   Frame 18 is the first speech frame after the loud update. */
+static void gen_state_resume(const char *path)
+{
+    FILE *f = open_out(path, "#!AMR\n");
+    int i;
+
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    write_nb_sid(f, 8, 1);
+    for (i = 0; i < 4; i++)
+        write_nodata(f, 1);
+    write_nb_sid(f, 32, 1);
+    for (i = 0; i < 4; i++)
+        write_nodata(f, 1);
+    for (i = 0; i < 4; i++)
+        write_nb_speech(f);
+    if (fclose(f))
+        die(path);
+}
+
+/* Speech, then one NO_DATA or reserved frame, repeated.
+   q1_at_first selects the Q bit of the first NO_DATA frame only.
+   Frame 8 is that NO_DATA, frame 17 is a Q=0 NO_DATA, frame 26 is reserved. */
+static void gen_nodata(const char *path, int q1_at_first)
+{
+    FILE *f = open_out(path, "#!AMR\n");
+    int i;
+
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    write_nodata(f, q1_at_first);
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    write_nodata(f, 0);
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    write_reserved(f);
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    if (fclose(f))
+        die(path);
+}
+
+/* 8 speech, one lost frame, SID_UPDATE, 4 NO_DATA, 4 speech.
+   Frame 10 is the first comfort-noise frame after that SID. */
+static void gen_sid_after_nodata(const char *path)
+{
+    FILE *f = open_out(path, "#!AMR\n");
+    int i;
+
+    for (i = 0; i < 8; i++)
+        write_nb_speech(f);
+    write_nodata(f, 1);
+    write_nb_sid(f, 20, 1);
+    for (i = 0; i < 4; i++)
+        write_nodata(f, 1);
+    for (i = 0; i < 4; i++)
+        write_nb_speech(f);
+    if (fclose(f))
+        die(path);
+}
+
+/* 11 speech, one SID, 51 NO_DATA (past the 50-frame mute threshold), 2 speech.
+   Eleven speech frames (44 subframes) take fixed_gain_smooth() past its
+   40-subframe hangover before silence begins. Frame 63 is the first speech
+   frame after the muted period. 65 frames * 160 samples = 10400. */
+static void gen_mute_resume(const char *path)
+{
+    FILE *f = open_out(path, "#!AMR\n");
+    int i;
+
+    for (i = 0; i < 11; i++)
+        write_nb_speech(f);
+    write_nb_sid(f, 15, 1);
+    for (i = 0; i < 51; i++)
+        write_nodata(f, 1);
+    for (i = 0; i < 2; i++)
+        write_nb_speech(f);
+    if (fclose(f))
+        die(path);
+}
+
+/* One SID_UPDATE and 10 NO_DATA frames. 11 * 320 = 3520 samples. */
+static void gen_wb(const char *path)
+{
+    FILE *f = open_out(path, "#!AMR-WB\n");
+    int i;
+
+    write_wb_sid(f, 20);
+    for (i = 0; i < 10; i++)
+        write_nodata(f, 1);
+    if (fclose(f))
+        die(path);
+}
+
+int main(int argc, char **argv)
+{
+    const char *base;
+
+    if (argc != 2) {
+        fprintf(stderr, "usage: amrgen <output.amr>\n");
+        return 1;
+    }
+
+    base = strrchr(argv[1], '/');
+    base = base ? base + 1 : argv[1];
+
+    if (!strcmp(base, "amrnb-sid-update.amr"))
+        gen_sid_update(argv[1]);
+    else if (!strcmp(base, "amrnb-state-resume-update.amr"))
+        gen_state_resume(argv[1]);
+    else if (!strcmp(base, "amrnb-nodata-cases.amr"))
+        gen_nodata(argv[1], 1);
+    else if (!strcmp(base, "amrnb-nodata-qswap-q0.amr"))
+        gen_nodata(argv[1], 0);
+    else if (!strcmp(base, "amrnb-sid-after-nodata.amr"))
+        gen_sid_after_nodata(argv[1]);
+    else if (!strcmp(base, "amrnb-dtx-mute-resume.amr"))
+        gen_mute_resume(argv[1]);
+    else if (!strcmp(base, "amrwb-dtx.amr"))
+        gen_wb(argv[1]);
+    else {
+        fprintf(stderr, "amrgen: unknown stream '%s'\n", base);
+        return 1;
+    }
+    return 0;
+}
diff --git a/tests/fate/amrwb.mak b/tests/fate/amrwb.mak
index aec1c9d859..3ffb934998 100644
--- a/tests/fate/amrwb.mak
+++ b/tests/fate/amrwb.mak
@@ -46,6 +46,22 @@ fate-amrwb-remux: CMD = transcode mov $(TARGET_SAMPLES)/amrwb/seed-23k85.awb amr

 $(FATE_AMRWB): CMP = stddev

+# Check that SID frames are decoded to comfort noise instead of being dropped,
+# so that a stream using DTX keeps its duration (see ticket #24921).
+FATE_AMRWB_DTX-$(call ALLYES, AMR_DEMUXER AMR_PARSER AMRWB_DECODER ARESAMPLE_FILTER \
+                              ASTATS_FILTER PCM_F32LE_ENCODER NULL_MUXER FILE_PROTOCOL) += fate-amrwb-dtx-duration
+FATE_AMRWB_DTX-$(call ALLYES, AMR_DEMUXER AMR_PARSER AMRWB_DECODER ARESAMPLE_FILTER \
+                              ASTATS_FILTER ATRIM_FILTER PCM_F32LE_ENCODER NULL_MUXER \
+                              FILE_PROTOCOL) += fate-amrwb-dtx-cn
+fate-amrwb-dtx-duration fate-amrwb-dtx-cn: tests/data/fate/amrwb-dtx.amr
+fate-amrwb-dtx-duration: CMD = ffmpeg -i $(TARGET_PATH)/tests/data/fate/amrwb-dtx.amr -af "aresample=osf=flt,astats=metadata=0:reset=0" -c:a pcm_f32le -f null -
+fate-amrwb-dtx-duration: CMP = grep
+fate-amrwb-dtx-duration: REF = Number of samples: 3520
+fate-amrwb-dtx-cn: CMD = ffmpeg -i $(TARGET_PATH)/tests/data/fate/amrwb-dtx.amr -af "atrim=start_sample=320:end_sample=3520,aresample=osf=flt,astats=metadata=0:reset=0" -c:a pcm_f32le -f null -
+fate-amrwb-dtx-cn: CMP = grep
+fate-amrwb-dtx-cn: REF = RMS level dB: -19\.
+
 FATE_SAMPLES_FFMPEG-$(call PCM, MOV, AMRWB, ARESAMPLE_FILTER) += $(FATE_AMRWB)
+FATE_FFMPEG += $(FATE_AMRWB_DTX-yes)
 FATE_SAMPLES_FFMPEG += $(FATE_AMRWB_REMUX-yes)
-fate-amrwb: $(FATE_AMRWB) $(FATE_AMRWB_REMUX-yes)
+fate-amrwb: $(FATE_AMRWB) $(FATE_AMRWB_REMUX-yes) $(FATE_AMRWB_DTX-yes)