//! LPCNet — Neural speech codec for Opus 1.4+.
//!
//! Matches the C reference: `lpcnet.c`, `lpcnet_enc.c`, `lpcnet_plc.c`,
//! `lpcnet_tables.c`, `freq.c`, `burg.c`, `kiss99.c`.
//!
//! All computation is IEEE 754 single-precision float unless noted.
//! Double precision is used where the C reference uses `double`.

use super::core::{
    ACTIVATION_LINEAR, ACTIVATION_SIGMOID, ACTIVATION_TANH, LinearLayer, WeightArray,
    compute_activation, compute_generic_conv1d, compute_generic_dense, compute_generic_gru,
    compute_linear, lin2ulaw, linear_init, lpcnet_exp, parse_weights, ulaw2lin,
};

// ===========================================================================
// Constants
// ===========================================================================

pub const NB_FEATURES: usize = 20;
pub const NB_TOTAL_FEATURES: usize = 36;
pub const LPCNET_FRAME_SIZE: usize = 160;

pub const LPC_ORDER: usize = 16;
pub const NB_BANDS: usize = 18;
pub const PREEMPHASIS_COEF: f32 = 0.85;

pub const FRAME_SIZE_5MS: usize = 2;
pub const OVERLAP_SIZE_5MS: usize = 2;
pub const TRAINING_OFFSET_5MS: usize = 1;
pub const WINDOW_SIZE_5MS: usize = FRAME_SIZE_5MS + OVERLAP_SIZE_5MS;
pub const FRAME_SIZE: usize = 80 * FRAME_SIZE_5MS;
pub const OVERLAP_SIZE: usize = 80 * OVERLAP_SIZE_5MS;
pub const TRAINING_OFFSET: usize = 80 * TRAINING_OFFSET_5MS;
pub const WINDOW_SIZE: usize = FRAME_SIZE + OVERLAP_SIZE;
pub const FREQ_SIZE: usize = WINDOW_SIZE / 2 + 1;

pub const PITCH_MIN_PERIOD: usize = 32;
pub const PITCH_MAX_PERIOD: usize = 256;
pub const PITCH_FRAME_SIZE: usize = 320;
pub const PITCH_BUF_SIZE: usize = PITCH_MAX_PERIOD + PITCH_FRAME_SIZE;
pub const PITCH_IF_MAX_FREQ: usize = 30;
pub const PITCH_IF_FEATURES: usize = 3 * PITCH_IF_MAX_FREQ - 2;

pub const PLC_MAX_FEC: usize = 104;
pub const CONT_VECTORS: usize = 5;
pub const PLC_BUF_SIZE: usize = (CONT_VECTORS + 10) * FRAME_SIZE;
pub const FEATURES_DELAY: usize = 1;
pub const MAX_FEATURE_BUFFER_SIZE: usize = 4;
pub const FARGAN_CONT_SAMPLES: usize = 320;

const PREEMPH: f32 = 0.85;
const FFT_SCALE: f32 = 1.0 / WINDOW_SIZE as f32;

const SILK_MAX_ORDER_LPC: usize = 16;
const FIND_LPC_COND_FAC: f64 = 1e-5;

// ===========================================================================
// KISS99 PRNG (matches kiss99.c)
// ===========================================================================

/// KISS99 PRNG state. Suitable for simulations, not cryptographic use.
#[derive(Clone, Debug, Default)]
pub struct Kiss99Ctx {
    pub z: u32,
    pub w: u32,
    pub jsr: u32,
    pub jcong: u32,
}

impl Kiss99Ctx {
    pub fn srand(&mut self, data: &[u8]) {
        self.z = 362436069;
        self.w = 521288629;
        self.jsr = 123456789;
        self.jcong = 380116160;
        let ndata = data.len();
        let mut i: usize = 3;
        while i < ndata {
            self.z ^= data[i - 3] as u32;
            self.w ^= data[i - 2] as u32;
            self.jsr ^= data[i - 1] as u32;
            self.jcong ^= data[i] as u32;
            self.rand();
            i += 4;
        }
        if i - 3 < ndata {
            self.z ^= data[i - 3] as u32;
        }
        if i - 2 < ndata {
            self.w ^= data[i - 2] as u32;
        }
        if i - 1 < ndata {
            self.jsr ^= data[i - 1] as u32;
        }
        // Fix potential short cycles (Rose 2018).
        if self.z == 0 || self.z == 0x9068FFFF {
            self.z += 1;
        }
        if self.w == 0 || self.w == 0x464FFFFF {
            self.w += 1;
        }
        if self.jsr == 0 {
            self.jsr += 1;
        }
    }

    pub fn rand(&mut self) -> u32 {
        let znew = 36969u32
            .wrapping_mul(self.z & 0xFFFF)
            .wrapping_add(self.z >> 16);
        let wnew = 18000u32
            .wrapping_mul(self.w & 0xFFFF)
            .wrapping_add(self.w >> 16);
        let mwc = (znew << 16).wrapping_add(wnew);
        // Swapped 13/17 from original 1999 to match KISS11 single maximal cycle.
        let mut shr3 = self.jsr ^ (self.jsr << 13);
        shr3 ^= shr3 >> 17;
        shr3 ^= shr3 << 5;
        let cong = 69069u32.wrapping_mul(self.jcong).wrapping_add(1234567);
        self.z = znew;
        self.w = wnew;
        self.jsr = shr3;
        self.jcong = cong;
        (mwc ^ cong).wrapping_add(shr3)
    }
}

// ===========================================================================
// Complex number type
// ===========================================================================

#[derive(Clone, Copy, Debug, Default)]
pub struct Cpx {
    pub r: f32,
    pub i: f32,
}

#[inline(always)]
fn cmul(a: Cpx, b: Cpx) -> Cpx {
    Cpx {
        r: a.r * b.r - a.i * b.i,
        i: a.r * b.i + a.i * b.r,
    }
}

/// Complex multiply with conjugate of second operand: a * conj(b).
#[inline(always)]
fn cmulc(a: Cpx, b: Cpx) -> Cpx {
    Cpx {
        r: a.r * b.r + a.i * b.i,
        i: a.i * b.r - a.r * b.i,
    }
}

#[inline(always)]
fn cadd(a: Cpx, b: Cpx) -> Cpx {
    Cpx {
        r: a.r + b.r,
        i: a.i + b.i,
    }
}

#[inline(always)]
fn csub(a: Cpx, b: Cpx) -> Cpx {
    Cpx {
        r: a.r - b.r,
        i: a.i - b.i,
    }
}

// ===========================================================================
// FFT tables (from lpcnet_tables.c — 320-point mixed-radix)
// ===========================================================================

#[rustfmt::skip]
static FFT_BITREV: [u16; 320] = [
0, 64, 128, 192, 256, 16, 80, 144, 208, 272, 32, 96, 160, 224, 288,
48, 112, 176, 240, 304, 4, 68, 132, 196, 260, 20, 84, 148, 212, 276,
36, 100, 164, 228, 292, 52, 116, 180, 244, 308, 8, 72, 136, 200, 264,
24, 88, 152, 216, 280, 40, 104, 168, 232, 296, 56, 120, 184, 248, 312,
12, 76, 140, 204, 268, 28, 92, 156, 220, 284, 44, 108, 172, 236, 300,
60, 124, 188, 252, 316, 1, 65, 129, 193, 257, 17, 81, 145, 209, 273,
33, 97, 161, 225, 289, 49, 113, 177, 241, 305, 5, 69, 133, 197, 261,
21, 85, 149, 213, 277, 37, 101, 165, 229, 293, 53, 117, 181, 245, 309,
9, 73, 137, 201, 265, 25, 89, 153, 217, 281, 41, 105, 169, 233, 297,
57, 121, 185, 249, 313, 13, 77, 141, 205, 269, 29, 93, 157, 221, 285,
45, 109, 173, 237, 301, 61, 125, 189, 253, 317, 2, 66, 130, 194, 258,
18, 82, 146, 210, 274, 34, 98, 162, 226, 290, 50, 114, 178, 242, 306,
6, 70, 134, 198, 262, 22, 86, 150, 214, 278, 38, 102, 166, 230, 294,
54, 118, 182, 246, 310, 10, 74, 138, 202, 266, 26, 90, 154, 218, 282,
42, 106, 170, 234, 298, 58, 122, 186, 250, 314, 14, 78, 142, 206, 270,
30, 94, 158, 222, 286, 46, 110, 174, 238, 302, 62, 126, 190, 254, 318,
3, 67, 131, 195, 259, 19, 83, 147, 211, 275, 35, 99, 163, 227, 291,
51, 115, 179, 243, 307, 7, 71, 135, 199, 263, 23, 87, 151, 215, 279,
39, 103, 167, 231, 295, 55, 119, 183, 247, 311, 11, 75, 139, 203, 267,
27, 91, 155, 219, 283, 43, 107, 171, 235, 299, 59, 123, 187, 251, 315,
15, 79, 143, 207, 271, 31, 95, 159, 223, 287, 47, 111, 175, 239, 303,
63, 127, 191, 255, 319,
];

/// 320-point FFT twiddle factors: W_N^k = exp(-j 2 pi k / 320).
/// Stored as (real, imag) pairs in a flat array for compactness.
#[rustfmt::skip]
static FFT_TWIDDLES: [(f32, f32); 320] = [
(1.00000000, -0.00000000), (0.999807239, -0.0196336918),
(0.999229014, -0.0392598175), (0.998265624, -0.0588708036),
(0.996917307, -0.0784590989), (0.995184720, -0.0980171412),
(0.993068457, -0.117537394), (0.990569353, -0.137012348),
(0.987688363, -0.156434461), (0.984426558, -0.175796285),
(0.980785251, -0.195090324), (0.976765871, -0.214309156),
(0.972369909, -0.233445361), (0.967599094, -0.252491564),
(0.962455213, -0.271440446), (0.956940353, -0.290284663),
(0.951056540, -0.309017003), (0.944806039, -0.327630192),
(0.938191354, -0.346117049), (0.931214929, -0.364470512),
(0.923879504, -0.382683426), (0.916187942, -0.400748819),
(0.908143163, -0.418659747), (0.899748266, -0.436409235),
(0.891006529, -0.453990489), (0.881921291, -0.471396744),
(0.872496009, -0.488621235), (0.862734377, -0.505657375),
(0.852640152, -0.522498548), (0.842217207, -0.539138317),
(0.831469595, -0.555570245), (0.820401430, -0.571787953),
(0.809017003, -0.587785244), (0.797320664, -0.603555918),
(0.785316944, -0.619093955), (0.773010433, -0.634393275),
(0.760405958, -0.649448037), (0.747508347, -0.664252460),
(0.734322488, -0.678800762), (0.720853567, -0.693087339),
(0.707106769, -0.707106769), (0.693087339, -0.720853567),
(0.678800762, -0.734322488), (0.664252460, -0.747508347),
(0.649448037, -0.760405958), (0.634393275, -0.773010433),
(0.619093955, -0.785316944), (0.603555918, -0.797320664),
(0.587785244, -0.809017003), (0.571787953, -0.820401430),
(0.555570245, -0.831469595), (0.539138317, -0.842217207),
(0.522498548, -0.852640152), (0.505657375, -0.862734377),
(0.488621235, -0.872496009), (0.471396744, -0.881921291),
(0.453990489, -0.891006529), (0.436409235, -0.899748266),
(0.418659747, -0.908143163), (0.400748819, -0.916187942),
(0.382683426, -0.923879504), (0.364470512, -0.931214929),
(0.346117049, -0.938191354), (0.327630192, -0.944806039),
(0.309017003, -0.951056540), (0.290284663, -0.956940353),
(0.271440446, -0.962455213), (0.252491564, -0.967599094),
(0.233445361, -0.972369909), (0.214309156, -0.976765871),
(0.195090324, -0.980785251), (0.175796285, -0.984426558),
(0.156434461, -0.987688363), (0.137012348, -0.990569353),
(0.117537394, -0.993068457), (0.0980171412, -0.995184720),
(0.0784590989, -0.996917307), (0.0588708036, -0.998265624),
(0.0392598175, -0.999229014), (0.0196336918, -0.999807239),
(6.12323426e-17, -1.00000000), (-0.0196336918, -0.999807239),
(-0.0392598175, -0.999229014), (-0.0588708036, -0.998265624),
(-0.0784590989, -0.996917307), (-0.0980171412, -0.995184720),
(-0.117537394, -0.993068457), (-0.137012348, -0.990569353),
(-0.156434461, -0.987688363), (-0.175796285, -0.984426558),
(-0.195090324, -0.980785251), (-0.214309156, -0.976765871),
(-0.233445361, -0.972369909), (-0.252491564, -0.967599094),
(-0.271440446, -0.962455213), (-0.290284663, -0.956940353),
(-0.309017003, -0.951056540), (-0.327630192, -0.944806039),
(-0.346117049, -0.938191354), (-0.364470512, -0.931214929),
(-0.382683426, -0.923879504), (-0.400748819, -0.916187942),
(-0.418659747, -0.908143163), (-0.436409235, -0.899748266),
(-0.453990489, -0.891006529), (-0.471396744, -0.881921291),
(-0.488621235, -0.872496009), (-0.505657375, -0.862734377),
(-0.522498548, -0.852640152), (-0.539138317, -0.842217207),
(-0.555570245, -0.831469595), (-0.571787953, -0.820401430),
(-0.587785244, -0.809017003), (-0.603555918, -0.797320664),
(-0.619093955, -0.785316944), (-0.634393275, -0.773010433),
(-0.649448037, -0.760405958), (-0.664252460, -0.747508347),
(-0.678800762, -0.734322488), (-0.693087339, -0.720853567),
(-0.707106769, -0.707106769), (-0.720853567, -0.693087339),
(-0.734322488, -0.678800762), (-0.747508347, -0.664252460),
(-0.760405958, -0.649448037), (-0.773010433, -0.634393275),
(-0.785316944, -0.619093955), (-0.797320664, -0.603555918),
(-0.809017003, -0.587785244), (-0.820401430, -0.571787953),
(-0.831469595, -0.555570245), (-0.842217207, -0.539138317),
(-0.852640152, -0.522498548), (-0.862734377, -0.505657375),
(-0.872496009, -0.488621235), (-0.881921291, -0.471396744),
(-0.891006529, -0.453990489), (-0.899748266, -0.436409235),
(-0.908143163, -0.418659747), (-0.916187942, -0.400748819),
(-0.923879504, -0.382683426), (-0.931214929, -0.364470512),
(-0.938191354, -0.346117049), (-0.944806039, -0.327630192),
(-0.951056540, -0.309017003), (-0.956940353, -0.290284663),
(-0.962455213, -0.271440446), (-0.967599094, -0.252491564),
(-0.972369909, -0.233445361), (-0.976765871, -0.214309156),
(-0.980785251, -0.195090324), (-0.984426558, -0.175796285),
(-0.987688363, -0.156434461), (-0.990569353, -0.137012348),
(-0.993068457, -0.117537394), (-0.995184720, -0.0980171412),
(-0.996917307, -0.0784590989), (-0.998265624, -0.0588708036),
(-0.999229014, -0.0392598175), (-0.999807239, -0.0196336918),
(-1.00000000, -1.22464685e-16), (-0.999807239, 0.0196336918),
(-0.999229014, 0.0392598175), (-0.998265624, 0.0588708036),
(-0.996917307, 0.0784590989), (-0.995184720, 0.0980171412),
(-0.993068457, 0.117537394), (-0.990569353, 0.137012348),
(-0.987688363, 0.156434461), (-0.984426558, 0.175796285),
(-0.980785251, 0.195090324), (-0.976765871, 0.214309156),
(-0.972369909, 0.233445361), (-0.967599094, 0.252491564),
(-0.962455213, 0.271440446), (-0.956940353, 0.290284663),
(-0.951056540, 0.309017003), (-0.944806039, 0.327630192),
(-0.938191354, 0.346117049), (-0.931214929, 0.364470512),
(-0.923879504, 0.382683426), (-0.916187942, 0.400748819),
(-0.908143163, 0.418659747), (-0.899748266, 0.436409235),
(-0.891006529, 0.453990489), (-0.881921291, 0.471396744),
(-0.872496009, 0.488621235), (-0.862734377, 0.505657375),
(-0.852640152, 0.522498548), (-0.842217207, 0.539138317),
(-0.831469595, 0.555570245), (-0.820401430, 0.571787953),
(-0.809017003, 0.587785244), (-0.797320664, 0.603555918),
(-0.785316944, 0.619093955), (-0.773010433, 0.634393275),
(-0.760405958, 0.649448037), (-0.747508347, 0.664252460),
(-0.734322488, 0.678800762), (-0.720853567, 0.693087339),
(-0.707106769, 0.707106769), (-0.693087339, 0.720853567),
(-0.678800762, 0.734322488), (-0.664252460, 0.747508347),
(-0.649448037, 0.760405958), (-0.634393275, 0.773010433),
(-0.619093955, 0.785316944), (-0.603555918, 0.797320664),
(-0.587785244, 0.809017003), (-0.571787953, 0.820401430),
(-0.555570245, 0.831469595), (-0.539138317, 0.842217207),
(-0.522498548, 0.852640152), (-0.505657375, 0.862734377),
(-0.488621235, 0.872496009), (-0.471396744, 0.881921291),
(-0.453990489, 0.891006529), (-0.436409235, 0.899748266),
(-0.418659747, 0.908143163), (-0.400748819, 0.916187942),
(-0.382683426, 0.923879504), (-0.364470512, 0.931214929),
(-0.346117049, 0.938191354), (-0.327630192, 0.944806039),
(-0.309017003, 0.951056540), (-0.290284663, 0.956940353),
(-0.271440446, 0.962455213), (-0.252491564, 0.967599094),
(-0.233445361, 0.972369909), (-0.214309156, 0.976765871),
(-0.195090324, 0.980785251), (-0.175796285, 0.984426558),
(-0.156434461, 0.987688363), (-0.137012348, 0.990569353),
(-0.117537394, 0.993068457), (-0.0980171412, 0.995184720),
(-0.0784590989, 0.996917307), (-0.0588708036, 0.998265624),
(-0.0392598175, 0.999229014), (-0.0196336918, 0.999807239),
(-1.83697015e-16, 1.00000000), (0.0196336918, 0.999807239),
(0.0392598175, 0.999229014), (0.0588708036, 0.998265624),
(0.0784590989, 0.996917307), (0.0980171412, 0.995184720),
(0.117537394, 0.993068457), (0.137012348, 0.990569353),
(0.156434461, 0.987688363), (0.175796285, 0.984426558),
(0.195090324, 0.980785251), (0.214309156, 0.976765871),
(0.233445361, 0.972369909), (0.252491564, 0.967599094),
(0.271440446, 0.962455213), (0.290284663, 0.956940353),
(0.309017003, 0.951056540), (0.327630192, 0.944806039),
(0.346117049, 0.938191354), (0.364470512, 0.931214929),
(0.382683426, 0.923879504), (0.400748819, 0.916187942),
(0.418659747, 0.908143163), (0.436409235, 0.899748266),
(0.453990489, 0.891006529), (0.471396744, 0.881921291),
(0.488621235, 0.872496009), (0.505657375, 0.862734377),
(0.522498548, 0.852640152), (0.539138317, 0.842217207),
(0.555570245, 0.831469595), (0.571787953, 0.820401430),
(0.587785244, 0.809017003), (0.603555918, 0.797320664),
(0.619093955, 0.785316944), (0.634393275, 0.773010433),
(0.649448037, 0.760405958), (0.664252460, 0.747508347),
(0.678800762, 0.734322488), (0.693087339, 0.720853567),
(0.707106769, 0.707106769), (0.720853567, 0.693087339),
(0.734322488, 0.678800762), (0.747508347, 0.664252460),
(0.760405958, 0.649448037), (0.773010433, 0.634393275),
(0.785316944, 0.619093955), (0.797320664, 0.603555918),
(0.809017003, 0.587785244), (0.820401430, 0.571787953),
(0.831469595, 0.555570245), (0.842217207, 0.539138317),
(0.852640152, 0.522498548), (0.862734377, 0.505657375),
(0.872496009, 0.488621235), (0.881921291, 0.471396744),
(0.891006529, 0.453990489), (0.899748266, 0.436409235),
(0.908143163, 0.418659747), (0.916187942, 0.400748819),
(0.923879504, 0.382683426), (0.931214929, 0.364470512),
(0.938191354, 0.346117049), (0.944806039, 0.327630192),
(0.951056540, 0.309017003), (0.956940353, 0.290284663),
(0.962455213, 0.271440446), (0.967599094, 0.252491564),
(0.972369909, 0.233445361), (0.976765871, 0.214309156),
(0.980785251, 0.195090324), (0.984426558, 0.175796285),
(0.987688363, 0.156434461), (0.990569353, 0.137012348),
(0.993068457, 0.117537394), (0.995184720, 0.0980171412),
(0.996917307, 0.0784590989), (0.998265624, 0.0588708036),
(0.999229014, 0.0392598175), (0.999807239, 0.0196336918),
];

#[inline]
fn tw(idx: usize) -> Cpx {
    let (r, i) = FFT_TWIDDLES[idx % 320];
    Cpx { r, i }
}

// ===========================================================================
// FFT implementation (320-point, radix-4 + radix-5, matches _kiss_fft_guts.h)
// ===========================================================================

/// Radix-4 DIT butterfly.
fn kf_bfly4(fout: &mut [Cpx], fstride: usize, m: usize, n: usize, mm: usize) {
    let m2 = 2 * m;
    let m3 = 3 * m;
    for i in 0..n {
        let base = i * mm;
        let mut tw1i = 0usize;
        let mut tw2i = 0usize;
        let mut tw3i = 0usize;
        for j in 0..m {
            let s0 = cmul(fout[base + m + j], tw(tw1i));
            let s1 = cmul(fout[base + m2 + j], tw(tw2i));
            let s2 = cmul(fout[base + m3 + j], tw(tw3i));
            let f0 = fout[base + j];
            let s5 = csub(f0, s1);
            let f0p = cadd(f0, s1);
            let s3 = cadd(s0, s2);
            let s4 = csub(s0, s2);
            fout[base + m2 + j] = csub(f0p, s3);
            fout[base + j] = cadd(f0p, s3);
            fout[base + m + j] = Cpx {
                r: s5.r + s4.i,
                i: s5.i - s4.r,
            };
            fout[base + m3 + j] = Cpx {
                r: s5.r - s4.i,
                i: s5.i + s4.r,
            };
            tw1i += fstride;
            tw2i += fstride * 2;
            tw3i += fstride * 3;
        }
    }
}

/// Radix-5 DIT butterfly.
fn kf_bfly5(fout: &mut [Cpx], fstride: usize, m: usize, n: usize, mm: usize) {
    let ya = tw(fstride * m);
    let yb = tw(fstride * 2 * m);
    for i in 0..n {
        let f0 = i * mm;
        let f1 = f0 + m;
        let f2 = f0 + 2 * m;
        let f3 = f0 + 3 * m;
        let f4 = f0 + 4 * m;
        for u in 0..m {
            let s0 = fout[f0 + u];
            let s1 = cmul(fout[f1 + u], tw(u * fstride));
            let s2 = cmul(fout[f2 + u], tw(2 * u * fstride));
            let s3 = cmul(fout[f3 + u], tw(3 * u * fstride));
            let s4 = cmul(fout[f4 + u], tw(4 * u * fstride));
            let s7 = cadd(s1, s4);
            let s10 = csub(s1, s4);
            let s8 = cadd(s2, s3);
            let s9 = csub(s2, s3);
            fout[f0 + u].r = s0.r + s7.r + s8.r;
            fout[f0 + u].i = s0.i + s7.i + s8.i;
            let s5 = Cpx {
                r: s0.r + s7.r * ya.r + s8.r * yb.r,
                i: s0.i + s7.i * ya.r + s8.i * yb.r,
            };
            let s6 = Cpx {
                r: s10.i * ya.i + s9.i * yb.i,
                i: -(s10.r * ya.i) - s9.r * yb.i,
            };
            fout[f1 + u] = csub(s5, s6);
            fout[f4 + u] = cadd(s5, s6);
            let s11 = Cpx {
                r: s0.r + s7.r * yb.r + s8.r * ya.r,
                i: s0.i + s7.i * yb.r + s8.i * ya.r,
            };
            let s12 = Cpx {
                r: -(s10.i * yb.i) + s9.i * ya.i,
                i: s10.r * yb.i - s9.r * ya.i,
            };
            fout[f2 + u] = cadd(s11, s12);
            fout[f3 + u] = csub(s11, s12);
        }
    }
}

/// 320-point FFT. Input is scaled by 1/320, bit-reversed, then four butterfly stages.
/// Matches `opus_fft_c` + `opus_fft_impl` for the kfft state.
fn opus_fft_320(fin: &[Cpx; WINDOW_SIZE], fout: &mut [Cpx; WINDOW_SIZE]) {
    // Bit-reverse copy with 1/N scaling.
    for i in 0..WINDOW_SIZE {
        let dst = FFT_BITREV[i] as usize;
        fout[dst] = Cpx {
            r: fin[i].r * FFT_SCALE,
            i: fin[i].i * FFT_SCALE,
        };
    }
    // Stage 1: radix-4, fstride=80, m=1, N=80, mm=4
    kf_bfly4(fout, 80, 1, 80, 4);
    // Stage 2: radix-4, fstride=20, m=4, N=20, mm=16
    kf_bfly4(fout, 20, 4, 20, 16);
    // Stage 3: radix-4, fstride=5, m=16, N=5, mm=64
    kf_bfly4(fout, 5, 16, 5, 64);
    // Stage 4: radix-5, fstride=1, m=64, N=1, mm=1
    kf_bfly5(fout, 1, 64, 1, 1);
}

// ===========================================================================
// Frequency-domain tables (from lpcnet_tables.c, freq.c)
// ===========================================================================

#[rustfmt::skip]
static HALF_WINDOW: [f32; OVERLAP_SIZE] = [
3.78491532e-05, 0.000340620492, 0.000946046319, 0.00185389258, 0.00306380726,
0.00457531959, 0.00638783723, 0.00850064680, 0.0109129101, 0.0136236614,
0.0166318044, 0.0199361145, 0.0235352255, 0.0274276342, 0.0316116922,
0.0360856056, 0.0408474281, 0.0458950549, 0.0512262285, 0.0568385124,
0.0627293140, 0.0688958541, 0.0753351897, 0.0820441842, 0.0890194997,
0.0962576419, 0.103754878, 0.111507311, 0.119510807, 0.127761051,
0.136253506, 0.144983411, 0.153945804, 0.163135484, 0.172547072,
0.182174906, 0.192013159, 0.202055752, 0.212296382, 0.222728521,
0.233345464, 0.244140238, 0.255105674, 0.266234398, 0.277518868,
0.288951218, 0.300523549, 0.312227666, 0.324055225, 0.335997701,
0.348046392, 0.360192508, 0.372427016, 0.384740859, 0.397124738,
0.409569323, 0.422065198, 0.434602767, 0.447172493, 0.459764689,
0.472369671, 0.484977663, 0.497579008, 0.510163903, 0.522722721,
0.535245717, 0.547723293, 0.560145974, 0.572504222, 0.584788740,
0.596990347, 0.609099925, 0.621108532, 0.633007407, 0.644788086,
0.656442165, 0.667961538, 0.679338276, 0.690564752, 0.701633692,
0.712537885, 0.723270535, 0.733825266, 0.744195819, 0.754376352,
0.764361382, 0.774145722, 0.783724606, 0.793093503, 0.802248418,
0.811185598, 0.819901764, 0.828393936, 0.836659551, 0.844696403,
0.852502763, 0.860077202, 0.867418647, 0.874526560, 0.881400526,
0.888040781, 0.894447744, 0.900622249, 0.906565487, 0.912279010,
0.917764664, 0.923024654, 0.928061485, 0.932878017, 0.937477291,
0.941862822, 0.946038187, 0.950007319, 0.953774393, 0.957343817,
0.960720181, 0.963908315, 0.966913164, 0.969739914, 0.972393870,
0.974880517, 0.977205336, 0.979374051, 0.981392324, 0.983266115,
0.985001266, 0.986603677, 0.988079309, 0.989434063, 0.990674019,
0.991804957, 0.992832899, 0.993763626, 0.994602919, 0.995356441,
0.996029854, 0.996628702, 0.997158289, 0.997623861, 0.998030603,
0.998383403, 0.998687088, 0.998946249, 0.999165416, 0.999348700,
0.999500215, 0.999623775, 0.999723017, 0.999801278, 0.999861658,
0.999907196, 0.999940455, 0.999963880, 0.999979615, 0.999989510,
0.999995291, 0.999998271, 0.999999523, 0.999999940, 1.00000000,
];

#[rustfmt::skip]
static DCT_TABLE: [f32; NB_BANDS * NB_BANDS] = [
0.707106769, 0.996194720, 0.984807730, 0.965925813, 0.939692616,
0.906307817, 0.866025388, 0.819152057, 0.766044438, 0.707106769,
0.642787635, 0.573576450, 0.500000000, 0.422618270, 0.342020154,
0.258819044, 0.173648179, 0.0871557444, 0.707106769, 0.965925813,
0.866025388, 0.707106769, 0.500000000, 0.258819044, 6.12323426e-17,
-0.258819044, -0.500000000, -0.707106769, -0.866025388, -0.965925813,
-1.00000000, -0.965925813, -0.866025388, -0.707106769, -0.500000000,
-0.258819044, 0.707106769, 0.906307817, 0.642787635, 0.258819044,
-0.173648179, -0.573576450, -0.866025388, -0.996194720, -0.939692616,
-0.707106769, -0.342020154, 0.0871557444, 0.500000000, 0.819152057,
0.984807730, 0.965925813, 0.766044438, 0.422618270, 0.707106769,
0.819152057, 0.342020154, -0.258819044, -0.766044438, -0.996194720,
-0.866025388, -0.422618270, 0.173648179, 0.707106769, 0.984807730,
0.906307817, 0.500000000, -0.0871557444, -0.642787635, -0.965925813,
-0.939692616, -0.573576450, 0.707106769, 0.707106769, 6.12323426e-17,
-0.707106769, -1.00000000, -0.707106769, -1.83697015e-16, 0.707106769,
1.00000000, 0.707106769, 3.06161700e-16, -0.707106769, -1.00000000,
-0.707106769, -4.28626385e-16, 0.707106769, 1.00000000, 0.707106769,
0.707106769, 0.573576450, -0.342020154, -0.965925813, -0.766044438,
0.0871557444, 0.866025388, 0.906307817, 0.173648179, -0.707106769,
-0.984807730, -0.422618270, 0.500000000, 0.996194720, 0.642787635,
-0.258819044, -0.939692616, -0.819152057, 0.707106769, 0.422618270,
-0.642787635, -0.965925813, -0.173648179, 0.819152057, 0.866025388,
-0.0871557444, -0.939692616, -0.707106769, 0.342020154, 0.996194720,
0.500000000, -0.573576450, -0.984807730, -0.258819044, 0.766044438,
0.906307817, 0.707106769, 0.258819044, -0.866025388, -0.707106769,
0.500000000, 0.965925813, 3.06161700e-16, -0.965925813, -0.500000000,
0.707106769, 0.866025388, -0.258819044, -1.00000000, -0.258819044,
0.866025388, 0.707106769, -0.500000000, -0.965925813, 0.707106769,
0.0871557444, -0.984807730, -0.258819044, 0.939692616, 0.422618270,
-0.866025388, -0.573576450, 0.766044438, 0.707106769, -0.642787635,
-0.819152057, 0.500000000, 0.906307817, -0.342020154, -0.965925813,
0.173648179, 0.996194720, 0.707106769, -0.0871557444, -0.984807730,
0.258819044, 0.939692616, -0.422618270, -0.866025388, 0.573576450,
0.766044438, -0.707106769, -0.642787635, 0.819152057, 0.500000000,
-0.906307817, -0.342020154, 0.965925813, 0.173648179, -0.996194720,
0.707106769, -0.258819044, -0.866025388, 0.707106769, 0.500000000,
-0.965925813, -4.28626385e-16, 0.965925813, -0.500000000, -0.707106769,
0.866025388, 0.258819044, -1.00000000, 0.258819044, 0.866025388,
-0.707106769, -0.500000000, 0.965925813, 0.707106769, -0.422618270,
-0.642787635, 0.965925813, -0.173648179, -0.819152057, 0.866025388,
0.0871557444, -0.939692616, 0.707106769, 0.342020154, -0.996194720,
0.500000000, 0.573576450, -0.984807730, 0.258819044, 0.766044438,
-0.906307817, 0.707106769, -0.573576450, -0.342020154, 0.965925813,
-0.766044438, -0.0871557444, 0.866025388, -0.906307817, 0.173648179,
0.707106769, -0.984807730, 0.422618270, 0.500000000, -0.996194720,
0.642787635, 0.258819044, -0.939692616, 0.819152057, 0.707106769,
-0.707106769, -1.83697015e-16, 0.707106769, -1.00000000, 0.707106769,
5.51091070e-16, -0.707106769, 1.00000000, -0.707106769, -2.69484189e-15,
0.707106769, -1.00000000, 0.707106769, -4.90477710e-16, -0.707106769,
1.00000000, -0.707106769, 0.707106769, -0.819152057, 0.342020154,
0.258819044, -0.766044438, 0.996194720, -0.866025388, 0.422618270,
0.173648179, -0.707106769, 0.984807730, -0.906307817, 0.500000000,
0.0871557444, -0.642787635, 0.965925813, -0.939692616, 0.573576450,
0.707106769, -0.906307817, 0.642787635, -0.258819044, -0.173648179,
0.573576450, -0.866025388, 0.996194720, -0.939692616, 0.707106769,
-0.342020154, -0.0871557444, 0.500000000, -0.819152057, 0.984807730,
-0.965925813, 0.766044438, -0.422618270, 0.707106769, -0.965925813,
0.866025388, -0.707106769, 0.500000000, -0.258819044, 1.10280111e-15,
0.258819044, -0.500000000, 0.707106769, -0.866025388, 0.965925813,
-1.00000000, 0.965925813, -0.866025388, 0.707106769, -0.500000000,
0.258819044, 0.707106769, -0.996194720, 0.984807730, -0.965925813,
0.939692616, -0.906307817, 0.866025388, -0.819152057, 0.766044438,
-0.707106769, 0.642787635, -0.573576450, 0.500000000, -0.422618270,
0.342020154, -0.258819044, 0.173648179, -0.0871557444,
];

/// Bark-scale band edge indices in 5 ms units.
static EBAND5MS: [usize; NB_BANDS] = [
    0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 34, 40,
];

static COMPENSATION: [f32; NB_BANDS] = [
    0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.666667, 0.5, 0.5, 0.5, 0.333333, 0.25, 0.25, 0.2,
    0.166667, 0.173913,
];

static ATT_TABLE: [f32; 10] = [0.0, 0.0, -0.2, -0.2, -0.4, -0.4, -0.8, -0.8, -1.6, -1.6];

// ===========================================================================
// Frequency analysis functions (matches freq.c)
// ===========================================================================

#[inline]
fn celt_log10_f32(x: f32) -> f32 {
    // C computes `celt_log10(x)` which expands (without FLOAT_APPROX, as
    // harness-deep-plc's config.h does) to:
    //   celt_log10(x) = 0.3010299957f * celt_log2(x)
    //   celt_log2(x)  = (float)(1.442695040888963387 * log((double)x))
    // Natural log done in double, rescaled, rounded to float, then scaled
    // again by the f32 constant. Rust's `log2_approx` is the FLOAT_APPROX
    // path, which diverges. Emulate the two-stage non-FLOAT_APPROX path
    // so we match bit-for-bit. Used by `compute_frame_features` for
    // `if_features[0]` and `if_features[3*i]`.
    let log2_f32 = (1.442695040888963387_f64 * (x as f64).ln()) as f32;
    0.3010299957_f32 * log2_f32
}

fn fmax(a: f32, b: f32) -> f32 {
    if a > b { a } else { b }
}
fn fmin(a: f32, b: f32) -> f32 {
    if a < b { a } else { b }
}

pub fn apply_window(x: &mut [f32; WINDOW_SIZE]) {
    for i in 0..OVERLAP_SIZE {
        x[i] *= HALF_WINDOW[i];
        x[WINDOW_SIZE - 1 - i] *= HALF_WINDOW[i];
    }
}

pub fn forward_transform(out: &mut [Cpx], input: &[f32]) {
    let mut x = [Cpx::default(); WINDOW_SIZE];
    for i in 0..WINDOW_SIZE {
        x[i] = Cpx {
            r: input[i],
            i: 0.0,
        };
    }
    let mut y = [Cpx::default(); WINDOW_SIZE];
    opus_fft_320(&x, &mut y);
    out[..FREQ_SIZE].copy_from_slice(&y[..FREQ_SIZE]);
}

fn inverse_transform(out: &mut [f32; WINDOW_SIZE], input: &[Cpx]) {
    let mut x = [Cpx::default(); WINDOW_SIZE];
    for i in 0..FREQ_SIZE {
        x[i] = input[i];
    }
    for i in FREQ_SIZE..WINDOW_SIZE {
        x[i] = Cpx {
            r: x[WINDOW_SIZE - i].r,
            i: -x[WINDOW_SIZE - i].i,
        };
    }
    let mut y = [Cpx::default(); WINDOW_SIZE];
    opus_fft_320(&x, &mut y);
    out[0] = WINDOW_SIZE as f32 * y[0].r;
    for i in 1..WINDOW_SIZE {
        out[i] = WINDOW_SIZE as f32 * y[WINDOW_SIZE - i].r;
    }
}

pub fn lpcn_compute_band_energy(band_e: &mut [f32; NB_BANDS], x: &[Cpx]) {
    let mut sum = [0.0f32; NB_BANDS];
    for i in 0..NB_BANDS - 1 {
        let band_size = (EBAND5MS[i + 1] - EBAND5MS[i]) * WINDOW_SIZE_5MS;
        for j in 0..band_size {
            let bin = EBAND5MS[i] * WINDOW_SIZE_5MS + j;
            let tmp = x[bin].r * x[bin].r + x[bin].i * x[bin].i;
            let frac = j as f32 / band_size as f32;
            sum[i] += (1.0 - frac) * tmp;
            sum[i + 1] += frac * tmp;
        }
    }
    sum[0] *= 2.0;
    sum[NB_BANDS - 1] *= 2.0;
    band_e.copy_from_slice(&sum);
}

fn compute_band_energy_inverse(band_e: &mut [f32; NB_BANDS], x: &[Cpx]) {
    let mut sum = [0.0f32; NB_BANDS];
    for i in 0..NB_BANDS - 1 {
        let band_size = (EBAND5MS[i + 1] - EBAND5MS[i]) * WINDOW_SIZE_5MS;
        for j in 0..band_size {
            let bin = EBAND5MS[i] * WINDOW_SIZE_5MS + j;
            let tmp = x[bin].r * x[bin].r + x[bin].i * x[bin].i;
            let tmp = 1.0 / (tmp + 1e-9);
            let frac = j as f32 / band_size as f32;
            sum[i] += (1.0 - frac) * tmp;
            sum[i + 1] += frac * tmp;
        }
    }
    sum[0] *= 2.0;
    sum[NB_BANDS - 1] *= 2.0;
    band_e.copy_from_slice(&sum);
}

pub fn dct(out: &mut [f32], input: &[f32]) {
    // C computes `sum * sqrt(2./NB_BANDS)` in double and truncates to f32
    // at the store — matching that requires keeping `scale` at f64 and
    // doing the final multiply in f64 too, then casting back to f32.
    // Using an f32 pre-cast loses a ULP on some NB_BANDS values and
    // leaks through to `features[0]` bit-exactness (root cause of
    // Stage 8.6 LPCNet feature drift).
    let scale_f64 = (2.0f64 / NB_BANDS as f64).sqrt();
    for i in 0..NB_BANDS {
        let mut sum = 0.0f32;
        for j in 0..NB_BANDS {
            sum += input[j] * DCT_TABLE[j * NB_BANDS + i];
        }
        out[i] = (sum as f64 * scale_f64) as f32;
    }
}

fn idct(out: &mut [f32], input: &[f32]) {
    let scale_f64 = (2.0f64 / NB_BANDS as f64).sqrt();
    for i in 0..NB_BANDS {
        let mut sum = 0.0f32;
        for j in 0..NB_BANDS {
            sum += input[j] * DCT_TABLE[i * NB_BANDS + j];
        }
        out[i] = (sum as f64 * scale_f64) as f32;
    }
}

fn interp_band_gain(g: &mut [f32], band_e: &[f32]) {
    for v in g[..FREQ_SIZE].iter_mut() {
        *v = 0.0;
    }
    for i in 0..NB_BANDS - 1 {
        let band_size = (EBAND5MS[i + 1] - EBAND5MS[i]) * WINDOW_SIZE_5MS;
        for j in 0..band_size {
            let frac = j as f32 / band_size as f32;
            g[EBAND5MS[i] * WINDOW_SIZE_5MS + j] = (1.0 - frac) * band_e[i] + frac * band_e[i + 1];
        }
    }
}

/// Levinson-Durbin recursion from autocorrelation. Float path (no Q-shifts).
fn lpcn_lpc(lpc: &mut [f32], rc: &mut [f32], ac: &[f32], p: usize) -> f32 {
    let mut error = ac[0];
    for v in lpc[..p].iter_mut() {
        *v = 0.0;
    }
    for v in rc[..p].iter_mut() {
        *v = 0.0;
    }
    if ac[0] != 0.0 {
        for i in 0..p {
            let mut rr = 0.0f32;
            for j in 0..i {
                rr += lpc[j] * ac[i - j];
            }
            rr += ac[i + 1];
            let r = -rr / error;
            rc[i] = r;
            lpc[i] = r;
            for j in 0..(i + 1) >> 1 {
                let tmp1 = lpc[j];
                let tmp2 = lpc[i - 1 - j];
                lpc[j] = tmp1 + r * tmp2;
                lpc[i - 1 - j] = tmp2 + r * tmp1;
            }
            error -= r * r * error;
            if error < 0.001 * ac[0] {
                break;
            }
        }
    }
    error
}

fn lpc_from_bands(lpc: &mut [f32], ex: &[f32]) -> f32 {
    let mut ac = [0.0f32; LPC_ORDER + 1];
    let mut rc = [0.0f32; LPC_ORDER];
    let mut xr = [0.0f32; FREQ_SIZE];
    let mut x_auto = [Cpx::default(); FREQ_SIZE];
    let mut x_auto_time = [0.0f32; WINDOW_SIZE];
    interp_band_gain(&mut xr, ex);
    xr[FREQ_SIZE - 1] = 0.0;
    for i in 0..FREQ_SIZE {
        x_auto[i] = Cpx { r: xr[i], i: 0.0 };
    }
    inverse_transform(&mut x_auto_time, &x_auto);
    for i in 0..LPC_ORDER + 1 {
        ac[i] = x_auto_time[i];
    }
    // -40 dB noise floor.
    ac[0] += ac[0] * 1e-4 + 320.0 / 12.0 / 38.0;
    // Lag windowing.
    for i in 1..LPC_ORDER + 1 {
        ac[i] *= 1.0 - 6e-5 * (i * i) as f32;
    }
    lpcn_lpc(lpc, &mut rc, &ac, LPC_ORDER)
}

pub fn lpc_weighting(lpc: &mut [f32], gamma: f32) {
    let mut gi = gamma;
    for i in 0..LPC_ORDER {
        lpc[i] *= gi;
        gi *= gamma;
    }
}

pub fn lpc_from_cepstrum(lpc: &mut [f32], cepstrum: &[f32]) -> f32 {
    let mut ex = [0.0f32; NB_BANDS];
    let mut tmp = [0.0f32; NB_BANDS];
    tmp[..NB_BANDS].copy_from_slice(&cepstrum[..NB_BANDS]);
    tmp[0] += 4.0;
    idct(&mut ex, &tmp);
    for i in 0..NB_BANDS {
        ex[i] = 10.0f32.powf(ex[i]) * COMPENSATION[i];
    }
    lpc_from_bands(lpc, &ex)
}

// ===========================================================================
// Burg analysis (matches burg.c)
// ===========================================================================

fn silk_energy_flp(data: &[f32], size: usize) -> f64 {
    let mut result = 0.0f64;
    let mut i = 0;
    while i < size.saturating_sub(3) {
        result += data[i] as f64 * data[i] as f64
            + data[i + 1] as f64 * data[i + 1] as f64
            + data[i + 2] as f64 * data[i + 2] as f64
            + data[i + 3] as f64 * data[i + 3] as f64;
        i += 4;
    }
    while i < size {
        result += data[i] as f64 * data[i] as f64;
        i += 1;
    }
    result
}

fn silk_inner_product_flp(d1: &[f32], d2: &[f32], size: usize) -> f64 {
    let mut result = 0.0f64;
    let mut i = 0;
    while i < size.saturating_sub(3) {
        result += d1[i] as f64 * d2[i] as f64
            + d1[i + 1] as f64 * d2[i + 1] as f64
            + d1[i + 2] as f64 * d2[i + 2] as f64
            + d1[i + 3] as f64 * d2[i + 3] as f64;
        i += 4;
    }
    while i < size {
        result += d1[i] as f64 * d2[i] as f64;
        i += 1;
    }
    result
}

pub fn silk_burg_analysis(
    a: &mut [f32],
    x: &[f32],
    min_inv_gain: f32,
    subfr_length: usize,
    nb_subfr: usize,
    d: usize,
) -> f32 {
    debug_assert!(subfr_length * nb_subfr <= 384);
    let c0_full = silk_energy_flp(x, nb_subfr * subfr_length);
    let mut c_first = [0.0f64; SILK_MAX_ORDER_LPC];
    for s in 0..nb_subfr {
        let xp = s * subfr_length;
        for n in 1..d + 1 {
            c_first[n - 1] += silk_inner_product_flp(&x[xp..], &x[xp + n..], subfr_length - n);
        }
    }
    let mut c_last = [0.0f64; SILK_MAX_ORDER_LPC];
    c_last[..d].copy_from_slice(&c_first[..d]);
    let mut caf = [0.0f64; SILK_MAX_ORDER_LPC + 1];
    let mut cab = [0.0f64; SILK_MAX_ORDER_LPC + 1];
    caf[0] = c0_full + FIND_LPC_COND_FAC * c0_full + 1e-9;
    cab[0] = caf[0];
    let mut inv_gain = 1.0f64;
    let mut reached_max = false;
    let mut af = [0.0f64; SILK_MAX_ORDER_LPC];
    for n in 0..d {
        for s in 0..nb_subfr {
            let xp = s * subfr_length;
            let (mut tmp1, mut tmp2) = (x[xp + n] as f64, x[xp + subfr_length - n - 1] as f64);
            for k in 0..n {
                c_first[k] -= x[xp + n] as f64 * x[xp + n - k - 1] as f64;
                c_last[k] -=
                    x[xp + subfr_length - n - 1] as f64 * x[xp + subfr_length - n + k] as f64;
                let atmp = af[k];
                tmp1 += x[xp + n - k - 1] as f64 * atmp;
                tmp2 += x[xp + subfr_length - n + k] as f64 * atmp;
            }
            for k in 0..=n {
                caf[k] -= tmp1 * x[xp + n - k] as f64;
                cab[k] -= tmp2 * x[xp + subfr_length - n + k - 1] as f64;
            }
        }
        let (mut tmp1, mut tmp2) = (c_first[n], c_last[n]);
        for k in 0..n {
            let atmp = af[k];
            tmp1 += c_last[n - k - 1] * atmp;
            tmp2 += c_first[n - k - 1] * atmp;
        }
        caf[n + 1] = tmp1;
        cab[n + 1] = tmp2;
        let mut num = cab[n + 1];
        let mut nrg_b = cab[0];
        let mut nrg_f = caf[0];
        for k in 0..n {
            let atmp = af[k];
            num += cab[n - k] * atmp;
            nrg_b += cab[k + 1] * atmp;
            nrg_f += caf[k + 1] * atmp;
        }
        let mut rc = -2.0 * num / (nrg_f + nrg_b);
        let tmp1_inv = inv_gain * (1.0 - rc * rc);
        if tmp1_inv <= min_inv_gain as f64 {
            rc = (1.0 - min_inv_gain as f64 / inv_gain).sqrt();
            if num > 0.0 {
                rc = -rc;
            }
            inv_gain = min_inv_gain as f64;
            reached_max = true;
        } else {
            inv_gain = tmp1_inv;
        }
        for k in 0..(n + 1) >> 1 {
            let (t1, t2) = (af[k], af[n - k - 1]);
            af[k] = t1 + rc * t2;
            af[n - k - 1] = t2 + rc * t1;
        }
        af[n] = rc;
        if reached_max {
            for k in n + 1..d {
                af[k] = 0.0;
            }
            break;
        }
        for k in 0..=n + 1 {
            let t1 = caf[k];
            caf[k] += rc * cab[n + 1 - k];
            cab[n + 1 - k] += rc * t1;
        }
    }
    let nrg_f;
    if reached_max {
        for k in 0..d {
            a[k] = -(af[k] as f32);
        }
        let mut c0 = c0_full;
        for s in 0..nb_subfr {
            c0 -= silk_energy_flp(&x[s * subfr_length..], d);
        }
        nrg_f = c0 * inv_gain;
    } else {
        let mut nf = caf[0];
        let mut t1 = 1.0f64;
        for k in 0..d {
            let atmp = af[k];
            nf += caf[k + 1] * atmp;
            t1 += atmp * atmp;
            a[k] = -(atmp as f32);
        }
        nrg_f = nf - FIND_LPC_COND_FAC * c0_full * t1;
    }
    if nrg_f < 0.0 { 0.0 } else { nrg_f as f32 }
}

fn compute_burg_cepstrum(pcm: &[f32], cepstrum: &mut [f32], len: usize, order: usize) {
    debug_assert!(order <= LPC_ORDER && len <= FRAME_SIZE);
    let mut burg_in = [0.0f32; FRAME_SIZE];
    for i in 0..len - 1 {
        burg_in[i] = pcm[i + 1] - PREEMPHASIS_COEF * pcm[i];
    }
    let mut burg_lpc = [0.0f32; LPC_ORDER];
    let g = silk_burg_analysis(&mut burg_lpc, &burg_in, 1e-3, len - 1, 1, order);
    let g = g / (len as f32 - 2.0 * (order as f32 - 1.0));
    let mut xbuf = [0.0f32; WINDOW_SIZE];
    xbuf[0] = 1.0;
    for i in 0..order {
        xbuf[i + 1] = -burg_lpc[i] * 0.995f32.powi(i as i32 + 1);
    }
    let mut lpc_spec = vec![Cpx::default(); FREQ_SIZE];
    forward_transform(&mut lpc_spec, &xbuf);
    let mut eburg = [0.0f32; NB_BANDS];
    compute_band_energy_inverse(&mut eburg, &lpc_spec);
    let ws3 = (WINDOW_SIZE as f32) * (WINDOW_SIZE as f32) * (WINDOW_SIZE as f32);
    for i in 0..NB_BANDS {
        eburg[i] *= 0.45 * g * (1.0 / ws3);
    }
    let mut ly = [0.0f32; NB_BANDS];
    let (mut log_max, mut follow) = (-2.0f32, -2.0f32);
    for i in 0..NB_BANDS {
        ly[i] = (1e-2 + eburg[i]).log10();
        ly[i] = fmax(log_max - 8.0, fmax(follow - 2.5, ly[i]));
        log_max = fmax(log_max, ly[i]);
        follow = fmax(follow - 2.5, ly[i]);
    }
    dct(cepstrum, &ly);
    cepstrum[0] -= 4.0;
}

pub fn burg_cepstral_analysis(ceps: &mut [f32], x: &[f32]) {
    let mut c0 = [0.0f32; NB_BANDS];
    let mut c1 = [0.0f32; NB_BANDS];
    compute_burg_cepstrum(x, &mut c0, FRAME_SIZE / 2, LPC_ORDER);
    compute_burg_cepstrum(&x[FRAME_SIZE / 2..], &mut c1, FRAME_SIZE / 2, LPC_ORDER);
    for i in 0..NB_BANDS {
        ceps[i] = 0.5 * (c0[i] + c1[i]);
        ceps[NB_BANDS + i] = c0[i] - c1[i];
    }
}

// ===========================================================================
// Float-domain signal processing helpers
// ===========================================================================

#[inline]
fn celt_inner_prod_f32(x: &[f32], y: &[f32], n: usize) -> f32 {
    let mut sum = 0.0f32;
    for i in 0..n {
        sum += x[i] * y[i];
    }
    sum
}

fn celt_pitch_xcorr_f32(x: &[f32], y: &[f32], xcorr: &mut [f32], len: usize, max_pitch: usize) {
    for i in 0..max_pitch {
        xcorr[i] = celt_inner_prod_f32(x, &y[i..], len);
    }
}

/// FIR filter (float path). `x` points to the start of history; `y = x[0..n] + Σ num[j]*x[-(j+1)]`.
fn celt_fir_f32(x: &[f32], num: &[f32], y: &mut [f32], n: usize, ord: usize) {
    for i in 0..n {
        let mut sum = x[ord + i];
        for j in 0..ord {
            sum += num[j] * x[ord + i - j - 1];
        }
        y[i] = sum;
    }
}

pub fn preemphasis(y: &mut [f32], mem: &mut f32, x: &[f32], coef: f32, n: usize) {
    for i in 0..n {
        let yi = x[i] + *mem;
        *mem = -coef * x[i];
        y[i] = yi;
    }
}

fn biquad(y: &mut [f32], mem: &mut [f32; 2], x: &[f32], b: &[f32; 2], a: &[f32; 2], n: usize) {
    let (mut m0, mut m1) = (mem[0], mem[1]);
    for i in 0..n {
        let xi = x[i];
        let yi = xi + m0;
        let m0_old = m0;
        // Reordered to reduce dependency chains; +1e-30 forces ordering (no effect on output).
        m0 = (b[0] - a[0]) * xi + m1 - a[0] * m0;
        m1 = (b[1] - a[1]) * xi + 1e-30 - a[1] * m0_old;
        y[i] = yi;
    }
    mem[0] = m0;
    mem[1] = m1;
}

// ===========================================================================
// Model layer types
// ===========================================================================

/// 1D convolutional layer wrapping a LinearLayer.
#[derive(Clone, Debug, Default)]
pub struct Conv1dLayer {
    pub input_weights: LinearLayer,
    pub feature_size: usize,
    pub kernel_size: usize,
    pub activation: i32,
}

/// Embedding lookup table.
#[derive(Clone, Debug, Default)]
pub struct EmbeddingLayer {
    pub weights: Vec<f32>,
    pub dim: usize,
}

impl EmbeddingLayer {
    pub fn compute(&self, output: &mut [f32], idx: usize) {
        let start = idx * self.dim;
        if start + self.dim <= self.weights.len() {
            output[..self.dim].copy_from_slice(&self.weights[start..start + self.dim]);
        } else {
            output[..self.dim].fill(0.0);
        }
    }

    pub fn accum(&self, output: &mut [f32], idx: usize) {
        let start = idx * self.dim;
        if start + self.dim <= self.weights.len() {
            for i in 0..self.dim {
                output[i] += self.weights[start + i];
            }
        }
    }
}

/// Dual FC layer for probabilistic sampling.
#[derive(Clone, Debug, Default)]
pub struct MDenseLayer {
    pub fc1: LinearLayer,
    pub fc2: LinearLayer,
}

/// Full LPCNet model weights.
#[derive(Clone, Debug, Default)]
pub struct LPCNetModel {
    pub embed_pitch: EmbeddingLayer,
    pub feature_conv1: Conv1dLayer,
    pub feature_conv2: Conv1dLayer,
    pub feature_dense1: LinearLayer,
    pub feature_dense2: LinearLayer,
    pub gru_a_dense_feature: LinearLayer,
    pub gru_b_dense_feature: LinearLayer,
    pub gru_a_embed_sig: EmbeddingLayer,
    pub gru_a_embed_pred: EmbeddingLayer,
    pub gru_a_embed_exc: EmbeddingLayer,
    pub sparse_gru_a: LinearLayer,
    pub gru_b_recurrent: LinearLayer,
    pub gru_b_input_dense: LinearLayer,
    pub dual_fc: MDenseLayer,
}

/// Runtime neural network state (conv + GRU layers).
#[derive(Clone, Debug, Default)]
pub struct NNetState {
    pub feature_conv1_state: Vec<f32>,
    pub feature_conv2_state: Vec<f32>,
    pub gru_a_state: Vec<f32>,
    pub gru_b_state: Vec<f32>,
}

/// PLC prediction model weights.
#[derive(Clone, Debug, Default)]
pub struct PLCModel {
    pub plc_dense_in: LinearLayer,
    pub plc_gru1_input: LinearLayer,
    pub plc_gru1_recurrent: LinearLayer,
    pub plc_gru2_input: LinearLayer,
    pub plc_gru2_recurrent: LinearLayer,
    pub plc_dense_out: LinearLayer,
}

/// PLC prediction GRU state.
#[derive(Clone, Debug)]
pub struct PLCNetState {
    pub gru1_state: Vec<f32>,
    pub gru2_state: Vec<f32>,
}

impl Default for PLCNetState {
    /// Matches C `PLCNetState` in `reference/dnn/lpcnet_private.h:45-47`
    /// where the GRU states are fixed-size `float[PLC_GRU{1,2}_STATE_SIZE]`
    /// arrays (both 192, per `reference/dnn/plc_data.h:18,22`). Rust uses
    /// `Vec` for dynamic sizing elsewhere in the DNN port, so we size
    /// them here to keep `compute_generic_gru` — which slices
    /// `state[..recurrent_weights.nb_inputs]` — happy on a fresh state.
    fn default() -> Self {
        Self {
            gru1_state: vec![0.0; PLC_GRU1_STATE_SIZE],
            gru2_state: vec![0.0; PLC_GRU2_STATE_SIZE],
        }
    }
}

/// PLC GRU state dimensions — match `reference/dnn/plc_data.h:18,22`.
pub const PLC_GRU1_STATE_SIZE: usize = 192;
pub const PLC_GRU2_STATE_SIZE: usize = 192;

// PLC model dimensions — match the xiph dump_plc.py schema recorded in
// `reference/dnn/plc_data.c`'s `init_plcmodel` body. Holding them as
// named constants keeps the `init_plcmodel` port one-to-one with the C.
const PLC_DENSE_IN_IN: usize = 57;
const PLC_DENSE_IN_OUT: usize = 128;
const PLC_GRU1_INPUT_IN: usize = 128;
const PLC_GRU1_INPUT_OUT: usize = 576;
const PLC_GRU1_RECURRENT_IN: usize = 192;
const PLC_GRU1_RECURRENT_OUT: usize = 576;
const PLC_GRU2_INPUT_IN: usize = 192;
const PLC_GRU2_INPUT_OUT: usize = 576;
const PLC_GRU2_RECURRENT_IN: usize = 192;
const PLC_GRU2_RECURRENT_OUT: usize = 576;
const PLC_DENSE_OUT_IN: usize = 192;
const PLC_DENSE_OUT_OUT: usize = 20;

/// Populate a `PLCModel` from parsed named weight arrays.
///
/// Rust port of the auto-generated C `init_plcmodel` in
/// `reference/dnn/plc_data.c`. The seven `linear_init` invocations below
/// mirror the C one-to-one, including the exact weight-name strings — so
/// a weight blob produced by xiph's `write_lpcnet_weights` populates the
/// same layers with the same data.
///
/// Returns `Err(())` when any required weight is missing or has an
/// unexpected size. Matches C's behaviour: silent about extra (unknown)
/// weights, loud about missing/wrong-sized ones.
pub fn init_plcmodel(arrays: &[WeightArray]) -> Result<PLCModel, ()> {
    // plc_dense_in: float-only dense layer, with bias.
    let plc_dense_in = linear_init(
        arrays,
        Some("plc_dense_in_bias"),
        None,
        None,
        Some("plc_dense_in_weights_float"),
        None,
        None,
        None,
        PLC_DENSE_IN_IN,
        PLC_DENSE_IN_OUT,
    )?;

    // plc_dense_out: float-only dense layer, with bias.
    let plc_dense_out = linear_init(
        arrays,
        Some("plc_dense_out_bias"),
        None,
        None,
        Some("plc_dense_out_weights_float"),
        None,
        None,
        None,
        PLC_DENSE_OUT_IN,
        PLC_DENSE_OUT_OUT,
    )?;

    // plc_gru1_input: quantized GRU input projection with bias.
    let plc_gru1_input = linear_init(
        arrays,
        Some("plc_gru1_input_bias"),
        Some("plc_gru1_input_subias"),
        Some("plc_gru1_input_weights_int8"),
        Some("plc_gru1_input_weights_float"),
        None,
        None,
        Some("plc_gru1_input_scale"),
        PLC_GRU1_INPUT_IN,
        PLC_GRU1_INPUT_OUT,
    )?;

    // plc_gru1_recurrent: quantized GRU recurrent projection with bias.
    let plc_gru1_recurrent = linear_init(
        arrays,
        Some("plc_gru1_recurrent_bias"),
        Some("plc_gru1_recurrent_subias"),
        Some("plc_gru1_recurrent_weights_int8"),
        Some("plc_gru1_recurrent_weights_float"),
        None,
        None,
        Some("plc_gru1_recurrent_scale"),
        PLC_GRU1_RECURRENT_IN,
        PLC_GRU1_RECURRENT_OUT,
    )?;

    // plc_gru2_input: quantized GRU input projection with bias.
    let plc_gru2_input = linear_init(
        arrays,
        Some("plc_gru2_input_bias"),
        Some("plc_gru2_input_subias"),
        Some("plc_gru2_input_weights_int8"),
        Some("plc_gru2_input_weights_float"),
        None,
        None,
        Some("plc_gru2_input_scale"),
        PLC_GRU2_INPUT_IN,
        PLC_GRU2_INPUT_OUT,
    )?;

    // plc_gru2_recurrent: quantized GRU recurrent projection with bias.
    let plc_gru2_recurrent = linear_init(
        arrays,
        Some("plc_gru2_recurrent_bias"),
        Some("plc_gru2_recurrent_subias"),
        Some("plc_gru2_recurrent_weights_int8"),
        Some("plc_gru2_recurrent_weights_float"),
        None,
        None,
        Some("plc_gru2_recurrent_scale"),
        PLC_GRU2_RECURRENT_IN,
        PLC_GRU2_RECURRENT_OUT,
    )?;

    Ok(PLCModel {
        plc_dense_in,
        plc_gru1_input,
        plc_gru1_recurrent,
        plc_gru2_input,
        plc_gru2_recurrent,
        plc_dense_out,
    })
}

// ===========================================================================
// External module interfaces (PitchDNN, FARGAN)
// ===========================================================================

/// PitchDNN state — delegates to `super::pitchdnn::PitchDnnState` so the
/// LPCNet encoder's feature extractor gets a real neural pitch estimate
/// once weights are loaded.
///
/// Before weights are loaded, `compute()` returns 0.0. This mirrors the
/// C reference's behaviour with `USE_WEIGHTS_FILE`: the model blob is
/// mandatory for inference and `compute_pitchdnn` is only called after
/// `pitchdnn_load_model` succeeds. Returning 0.0 here keeps
/// `compute_frame_features` well-defined for tests that exercise the
/// encoder before loading weights.
#[derive(Clone, Debug)]
pub struct PitchDNNState {
    inner: super::pitchdnn::PitchDnnState,
    loaded: bool,
}

impl Default for PitchDNNState {
    fn default() -> Self {
        Self {
            inner: super::pitchdnn::PitchDnnState::new_empty(),
            loaded: false,
        }
    }
}

impl PitchDNNState {
    pub fn init(&mut self) {
        // `new_empty()` already produces the equivalent of C
        // `pitchdnn_init` in the `USE_WEIGHTS_FILE` path.
    }
    pub fn load_model(&mut self, data: &[u8]) -> i32 {
        match self.inner.load_model(data) {
            Ok(()) => {
                self.loaded = true;
                0
            }
            Err(e) => e,
        }
    }
    /// Build the inner `PitchDnn` model from pre-parsed arrays without
    /// mutating state. Caller commits via `apply_loaded_model`.
    /// Enables atomic multi-sub-model loads from a single parse.
    pub(crate) fn try_init(
        arrays: &[crate::dnn::core::WeightArray],
    ) -> Result<super::pitchdnn::PitchDnn, ()> {
        super::pitchdnn::init_pitchdnn(arrays)
    }
    /// Commit a pre-built `PitchDnn` model as the new weights.
    pub(crate) fn apply_loaded_model(&mut self, model: super::pitchdnn::PitchDnn) {
        self.inner.model = model;
        self.loaded = true;
    }
    pub fn compute(&mut self, if_features: &[f32], xcorr_features: &[f32]) -> f32 {
        if !self.loaded {
            return 0.0;
        }
        self.inner.compute(if_features, xcorr_features)
    }
}

/// FARGAN state — delegates to the real implementation in `super::fargan`.
#[derive(Clone, Debug, Default)]
pub struct FARGANState {
    inner: super::fargan::FarganState,
}

impl FARGANState {
    pub fn init(&mut self) {
        self.inner.init();
    }
    pub fn load_model(&mut self, data: &[u8]) -> i32 {
        self.inner.load_model(data)
    }
    /// Build the inner `FarganModel` from pre-parsed arrays without
    /// mutating state. Caller commits via `apply_loaded_model`.
    /// Enables atomic multi-sub-model loads from a single parse.
    pub(crate) fn try_init(
        arrays: &[crate::dnn::core::WeightArray],
    ) -> Result<super::fargan::FarganModel, ()> {
        super::fargan::init_fargan(arrays)
    }
    /// Commit a pre-built `FarganModel` as the new weights.
    pub(crate) fn apply_loaded_model(&mut self, model: super::fargan::FarganModel) {
        self.inner.model = model;
    }
    pub fn cont(&mut self, pcm: &[f32], features: &[f32]) {
        self.inner.cont(pcm, features);
    }
    pub fn synthesize_int(&mut self, pcm: &mut [i16], features: &[f32]) {
        self.inner.synthesize_int(pcm, features);
    }
}

// ===========================================================================
// LPCNet-specific compute functions
// ===========================================================================

fn compute_conv1d_layer(layer: &Conv1dLayer, output: &mut [f32], mem: &mut [f32], input: &[f32]) {
    compute_generic_conv1d(
        &layer.input_weights,
        output,
        mem,
        input,
        layer.feature_size,
        layer.activation,
    );
}

/// Dense layer with tanh activation. Matches C `_lpcnet_compute_dense`.
fn lpcnet_compute_dense(layer: &LinearLayer, output: &mut [f32], input: &[f32]) {
    compute_generic_dense(layer, output, input, ACTIVATION_TANH);
}

/// Sparse GRU: input is pre-computed 3*N, only recurrent weights applied here.
fn compute_sparse_gru(recurrent: &LinearLayer, state: &mut [f32], input: &[f32]) {
    let n = recurrent.nb_inputs;
    let mut zrh = vec![0.0f32; 3 * n];
    let mut recur = vec![0.0f32; 3 * n];
    zrh[..3 * n].copy_from_slice(&input[..3 * n]);
    compute_linear(recurrent, &mut recur, state);
    for i in 0..2 * n {
        zrh[i] += recur[i];
    }
    compute_activation(&mut zrh, 2 * n, ACTIVATION_SIGMOID);
    for i in 0..n {
        zrh[2 * n + i] += recur[2 * n + i] * zrh[n + i];
    }
    compute_activation(&mut zrh[2 * n..], n, ACTIVATION_TANH);
    for i in 0..n {
        state[i] = zrh[i] * state[i] + (1.0 - zrh[i]) * zrh[2 * n + i];
    }
}

/// GRU-B with extra input from GRU-A state.
fn compute_gru_b(
    recurrent: &LinearLayer,
    input_dense: &LinearLayer,
    gru_input: &mut [f32],
    state: &mut [f32],
    extra: &[f32],
) {
    let n = recurrent.nb_inputs;
    let mut recur = vec![0.0f32; 3 * n];
    compute_linear(recurrent, &mut recur, state);
    for i in 0..2 * n {
        gru_input[i] += recur[i];
    }
    compute_activation(gru_input, 2 * n, ACTIVATION_SIGMOID);
    for i in 0..n {
        gru_input[2 * n + i] += recur[2 * n + i] * gru_input[n + i];
    }
    // Add dense transform of extra input to candidate h.
    let mut dense_out = vec![0.0f32; n];
    compute_linear(input_dense, &mut dense_out, extra);
    for i in 0..n {
        gru_input[2 * n + i] += dense_out[i];
    }
    compute_activation(&mut gru_input[2 * n..], n, ACTIVATION_TANH);
    for i in 0..n {
        state[i] = gru_input[i] * state[i] + (1.0 - gru_input[i]) * gru_input[2 * n + i];
    }
}

/// Assemble GRU-A input: condition + three embedding lookups.
fn compute_gru_a_input(
    output: &mut [f32],
    condition: &[f32],
    size: usize,
    embed_sig: &EmbeddingLayer,
    last_sig: usize,
    embed_pred: &EmbeddingLayer,
    pred: usize,
    embed_exc: &EmbeddingLayer,
    last_exc: usize,
) {
    output[..3 * size].copy_from_slice(&condition[..3 * size]);
    embed_sig.accum(output, last_sig);
    embed_pred.accum(output, pred);
    embed_exc.accum(output, last_exc);
}

/// Dual FC sampling. Returns mu-law index 0..255.
fn sample_mdense(
    layer: &MDenseLayer,
    state: &[f32],
    logit_table: &[f32; 256],
    rng: &mut Kiss99Ctx,
) -> i32 {
    let mut out1 = vec![0.0f32; 256];
    let mut out2 = vec![0.0f32; 256];
    compute_linear(&layer.fc1, &mut out1, state);
    compute_linear(&layer.fc2, &mut out2, state);
    for i in 0..256 {
        out1[i] += logit_table[i];
        out2[i] += logit_table[i];
    }
    // Softmax (element-wise exp then normalize).
    for i in 0..256 {
        out1[i] = lpcnet_exp(out1[i]);
    }
    for i in 0..256 {
        out2[i] = lpcnet_exp(out2[i]);
    }
    let sum1: f32 = out1.iter().sum();
    let sum2: f32 = out2.iter().sum();
    let inv1 = if sum1 > 0.0 { 1.0 / sum1 } else { 0.0 };
    let inv2 = if sum2 > 0.0 { 1.0 / sum2 } else { 0.0 };
    // Average the two distributions and sample.
    let r = (rng.rand() as f64 + 0.5) / 4294967296.0;
    let mut cdf = 0.0f64;
    for i in 0..256 {
        cdf += 0.5 * (out1[i] * inv1 + out2[i] * inv2) as f64;
        if r < cdf {
            return i as i32;
        }
    }
    255
}

// ===========================================================================
// LPCNetEncState — Encoder / Feature Extractor (matches lpcnet_enc.c)
// ===========================================================================

#[derive(Clone, Debug)]
pub struct LPCNetEncState {
    pub pitchdnn: PitchDNNState,
    pub analysis_mem: [f32; OVERLAP_SIZE],
    pub mem_preemph: f32,
    pub prev_if: [Cpx; PITCH_IF_MAX_FREQ],
    pub if_features: [f32; PITCH_IF_FEATURES],
    pub xcorr_features: [f32; PITCH_MAX_PERIOD - PITCH_MIN_PERIOD],
    pub dnn_pitch: f32,
    pub pitch_mem: [f32; LPC_ORDER],
    pub pitch_filt: f32,
    pub exc_buf: [f32; PITCH_BUF_SIZE],
    pub lp_buf: [f32; PITCH_BUF_SIZE],
    pub lp_mem: [f32; 2],
    pub lpc: [f32; LPC_ORDER],
    pub features: [f32; NB_TOTAL_FEATURES],
    pub sig_mem: [f32; LPC_ORDER],
    pub burg_cepstrum: [f32; 2 * NB_BANDS],
}

impl Default for LPCNetEncState {
    fn default() -> Self {
        Self {
            pitchdnn: PitchDNNState::default(),
            analysis_mem: [0.0; OVERLAP_SIZE],
            mem_preemph: 0.0,
            prev_if: [Cpx::default(); PITCH_IF_MAX_FREQ],
            if_features: [0.0; PITCH_IF_FEATURES],
            xcorr_features: [0.0; PITCH_MAX_PERIOD - PITCH_MIN_PERIOD],
            dnn_pitch: 0.0,
            pitch_mem: [0.0; LPC_ORDER],
            pitch_filt: 0.0,
            exc_buf: [0.0; PITCH_BUF_SIZE],
            lp_buf: [0.0; PITCH_BUF_SIZE],
            lp_mem: [0.0; 2],
            lpc: [0.0; LPC_ORDER],
            features: [0.0; NB_TOTAL_FEATURES],
            sig_mem: [0.0; LPC_ORDER],
            burg_cepstrum: [0.0; 2 * NB_BANDS],
        }
    }
}

impl LPCNetEncState {
    pub fn new() -> Self {
        let mut st = Self::default();
        st.pitchdnn.init();
        st
    }

    pub fn load_model(&mut self, data: &[u8]) -> i32 {
        self.pitchdnn.load_model(data)
    }

    fn frame_analysis(&mut self, x_out: &mut [Cpx], ex: &mut [f32; NB_BANDS], input: &[f32]) {
        let mut x = [0.0f32; WINDOW_SIZE];
        x[..OVERLAP_SIZE].copy_from_slice(&self.analysis_mem);
        x[OVERLAP_SIZE..].copy_from_slice(&input[..FRAME_SIZE]);
        self.analysis_mem
            .copy_from_slice(&input[FRAME_SIZE - OVERLAP_SIZE..FRAME_SIZE]);
        apply_window(&mut x);
        forward_transform(x_out, &x);
        lpcn_compute_band_energy(ex, x_out);
    }

    pub fn compute_frame_features(&mut self, input: &[f32]) {
        let lp_b: [f32; 2] = [-0.84946, 1.0];
        let lp_a: [f32; 2] = [-1.54220, 0.70781];

        let mut aligned_in = [0.0f32; FRAME_SIZE];
        aligned_in[..TRAINING_OFFSET]
            .copy_from_slice(&self.analysis_mem[OVERLAP_SIZE - TRAINING_OFFSET..OVERLAP_SIZE]);

        let mut x_cpx = vec![Cpx::default(); FREQ_SIZE];
        let mut ex = [0.0f32; NB_BANDS];
        self.frame_analysis(&mut x_cpx, &mut ex, input);

        // Instantaneous frequency features.
        self.if_features[0] = fmax(
            -1.0,
            fmin(
                1.0,
                (1.0 / 64.0) * (10.0 * celt_log10_f32(1e-15 + x_cpx[0].r * x_cpx[0].r) - 6.0),
            ),
        );
        for i in 1..PITCH_IF_MAX_FREQ {
            let prod = cmulc(x_cpx[i], self.prev_if[i]);
            let norm_1 = 1.0 / (1e-15 + prod.r * prod.r + prod.i * prod.i).sqrt();
            let prod = Cpx {
                r: prod.r * norm_1,
                i: prod.i * norm_1,
            };
            self.if_features[3 * i - 2] = prod.r;
            self.if_features[3 * i - 1] = prod.i;
            self.if_features[3 * i] = fmax(
                -1.0,
                fmin(
                    1.0,
                    (1.0 / 64.0)
                        * (10.0
                            * celt_log10_f32(
                                1e-15 + x_cpx[i].r * x_cpx[i].r + x_cpx[i].i * x_cpx[i].i,
                            )
                            - 6.0),
                ),
            );
        }
        self.prev_if[..PITCH_IF_MAX_FREQ].copy_from_slice(&x_cpx[..PITCH_IF_MAX_FREQ]);

        // Cepstral coefficients with spectral floor.
        //
        // Bit-exactness with C depends on how `celt_log10` is compiled:
        // without `FLOAT_APPROX` (harness default) it expands to
        //   celt_log10(x) = 0.3010299957f * celt_log2(x)
        //   celt_log2(x)  = (float)(1.442695040888963387 * log(x))
        // — natural log done in double, rescaled, rounded to float,
        // then scaled again by the f32 constant. Rust's `f32::log10`
        // maps to libm `log10f` (single precision), which diverges.
        // Emulate the two-stage C path so we match bit-for-bit.
        let mut ly = [0.0f32; NB_BANDS];
        let (mut log_max, mut follow) = (-2.0f32, -2.0f32);
        for i in 0..NB_BANDS {
            let arg = 1e-2f32 + ex[i];
            let log2_f32 = (1.442695040888963387_f64 * (arg as f64).ln()) as f32;
            ly[i] = 0.3010299957_f32 * log2_f32;
            ly[i] = fmax(log_max - 8.0, fmax(follow - 2.5, ly[i]));
            log_max = fmax(log_max, ly[i]);
            follow = fmax(follow - 2.5, ly[i]);
        }
        dct(&mut self.features, &ly);
        self.features[0] -= 4.0;
        lpc_from_cepstrum(&mut self.lpc, &self.features);
        for i in 0..LPC_ORDER {
            self.features[NB_BANDS + 2 + i] = self.lpc[i];
        }

        // Shift pitch buffers.
        self.exc_buf.copy_within(FRAME_SIZE..PITCH_BUF_SIZE, 0);
        self.lp_buf.copy_within(FRAME_SIZE..PITCH_BUF_SIZE, 0);

        aligned_in[TRAINING_OFFSET..FRAME_SIZE]
            .copy_from_slice(&input[..FRAME_SIZE - TRAINING_OFFSET]);

        // LPC residual via FIR filter.
        let mut x_fir = [0.0f32; FRAME_SIZE + LPC_ORDER];
        x_fir[..LPC_ORDER].copy_from_slice(&self.pitch_mem);
        x_fir[LPC_ORDER..LPC_ORDER + FRAME_SIZE].copy_from_slice(&aligned_in);
        self.pitch_mem
            .copy_from_slice(&aligned_in[FRAME_SIZE - LPC_ORDER..FRAME_SIZE]);
        celt_fir_f32(
            &x_fir,
            &self.lpc,
            &mut self.lp_buf[PITCH_MAX_PERIOD..],
            FRAME_SIZE,
            LPC_ORDER,
        );

        // Pitch pre-filter.
        for i in 0..FRAME_SIZE {
            self.exc_buf[PITCH_MAX_PERIOD + i] =
                self.lp_buf[PITCH_MAX_PERIOD + i] + 0.7 * self.pitch_filt;
            self.pitch_filt = self.lp_buf[PITCH_MAX_PERIOD + i];
        }

        // Biquad LP filter on lp_buf.
        let mut lp_out = [0.0f32; FRAME_SIZE];
        biquad(
            &mut lp_out,
            &mut self.lp_mem,
            &self.lp_buf[PITCH_MAX_PERIOD..PITCH_MAX_PERIOD + FRAME_SIZE],
            &lp_b,
            &lp_a,
            FRAME_SIZE,
        );
        self.lp_buf[PITCH_MAX_PERIOD..PITCH_MAX_PERIOD + FRAME_SIZE].copy_from_slice(&lp_out);

        // Cross-correlation and pitch detection.
        let xcorr_len = PITCH_MAX_PERIOD - PITCH_MIN_PERIOD;
        let mut xcorr = [0.0f32; PITCH_MAX_PERIOD];
        celt_pitch_xcorr_f32(
            &self.exc_buf[PITCH_MAX_PERIOD..],
            &self.exc_buf,
            &mut xcorr,
            FRAME_SIZE,
            xcorr_len,
        );
        let ener0 = celt_inner_prod_f32(
            &self.exc_buf[PITCH_MAX_PERIOD..],
            &self.exc_buf[PITCH_MAX_PERIOD..],
            FRAME_SIZE,
        );
        let mut ener1: f64 = celt_inner_prod_f32(&self.exc_buf, &self.exc_buf, FRAME_SIZE) as f64;
        let mut ener_norm = [0.0f32; PITCH_MAX_PERIOD - PITCH_MIN_PERIOD];
        for i in 0..xcorr_len {
            let ener: f32 = ((1.0f32 + ener0) as f64 + ener1) as f32;
            self.xcorr_features[i] = 2.0 * xcorr[i];
            ener_norm[i] = ener;
            ener1 += self.exc_buf[i + FRAME_SIZE] as f64 * self.exc_buf[i + FRAME_SIZE] as f64
                - self.exc_buf[i] as f64 * self.exc_buf[i] as f64;
        }
        for i in 0..xcorr_len {
            self.xcorr_features[i] /= ener_norm[i];
        }

        self.dnn_pitch = self
            .pitchdnn
            .compute(&self.if_features, &self.xcorr_features);
        let pitch = (0.5 + 256.0 / 2.0f32.powf((1.0 / 60.0) * ((self.dnn_pitch + 1.5) * 60.0)))
            .floor() as i32;
        let pitch = pitch
            .max(PITCH_MIN_PERIOD as i32)
            .min(PITCH_MAX_PERIOD as i32 - 1) as usize;

        // Pitch correlation.
        let xx = celt_inner_prod_f32(
            &self.lp_buf[PITCH_MAX_PERIOD..],
            &self.lp_buf[PITCH_MAX_PERIOD..],
            FRAME_SIZE,
        );
        let yy = celt_inner_prod_f32(
            &self.lp_buf[PITCH_MAX_PERIOD - pitch..],
            &self.lp_buf[PITCH_MAX_PERIOD - pitch..],
            FRAME_SIZE,
        );
        let xy = celt_inner_prod_f32(
            &self.lp_buf[PITCH_MAX_PERIOD..],
            &self.lp_buf[PITCH_MAX_PERIOD - pitch..],
            FRAME_SIZE,
        );
        let mut frame_corr = xy / (1.0 + xx * yy).sqrt();
        // Soft-plus normalization.
        frame_corr = (1.0 + (5.0 * frame_corr).exp()).ln() / (1.0 + 5.0f32.exp()).ln();
        self.features[NB_BANDS] = self.dnn_pitch;
        self.features[NB_BANDS + 1] = frame_corr - 0.5;
    }

    fn compute_single_frame_features_impl(
        &mut self,
        x: &mut [f32],
        features: &mut [f32; NB_TOTAL_FEATURES],
    ) -> i32 {
        #[allow(clippy::unnecessary_to_owned)] // x is both output and input; copy required
        preemphasis(
            x,
            &mut self.mem_preemph,
            &x.to_vec(),
            PREEMPHASIS_COEF,
            FRAME_SIZE,
        );
        self.compute_frame_features(x);
        features.copy_from_slice(&self.features);
        0
    }

    pub fn compute_single_frame_features(
        &mut self,
        pcm: &[i16],
        features: &mut [f32; NB_TOTAL_FEATURES],
    ) -> i32 {
        let mut x = [0.0f32; FRAME_SIZE];
        for i in 0..FRAME_SIZE {
            x[i] = pcm[i] as f32;
        }
        self.compute_single_frame_features_impl(&mut x, features)
    }

    pub fn compute_single_frame_features_float(
        &mut self,
        pcm: &[f32],
        features: &mut [f32; NB_TOTAL_FEATURES],
    ) -> i32 {
        let mut x = [0.0f32; FRAME_SIZE];
        x[..FRAME_SIZE].copy_from_slice(&pcm[..FRAME_SIZE]);
        self.compute_single_frame_features_impl(&mut x, features)
    }
}

// ===========================================================================
// LPCNetState — Synthesis Core (matches lpcnet.c)
// ===========================================================================

#[derive(Clone, Debug)]
pub struct LPCNetState {
    pub model: LPCNetModel,
    pub nnet: NNetState,
    // Fields preserved across reset:
    pub sampling_logit_table: [f32; 256],
    // Fields cleared on reset:
    pub frame_count: i32,
    pub feature_buffer_fill: i32,
    pub feature_buffer: [f32; MAX_FEATURE_BUFFER_SIZE * NB_FEATURES],
    pub gru_a_condition: Vec<f32>,
    pub gru_b_condition: Vec<f32>,
    pub lpc: [f32; LPC_ORDER],
    pub last_sig: [f32; LPC_ORDER],
    pub last_exc: i32,
    pub deemph_mem: f32,
    pub rng: Kiss99Ctx,
    pub old_lpc: [[f32; LPC_ORDER]; FEATURES_DELAY],
}

impl LPCNetState {
    pub fn new() -> Self {
        let mut st = Self {
            model: LPCNetModel::default(),
            nnet: NNetState::default(),
            sampling_logit_table: [0.0; 256],
            frame_count: 0,
            feature_buffer_fill: 0,
            feature_buffer: [0.0; MAX_FEATURE_BUFFER_SIZE * NB_FEATURES],
            gru_a_condition: Vec::new(),
            gru_b_condition: Vec::new(),
            lpc: [0.0; LPC_ORDER],
            last_sig: [0.0; LPC_ORDER],
            last_exc: 0,
            deemph_mem: 0.0,
            rng: Kiss99Ctx::default(),
            old_lpc: [[0.0; LPC_ORDER]; FEATURES_DELAY],
        };
        st.init();
        st
    }

    pub fn init(&mut self) {
        for i in 0..256 {
            let prob = 0.025f32 + 0.95 * i as f32 / 255.0;
            self.sampling_logit_table[i] = -((1.0 - prob) / prob).ln();
        }
        self.reset();
    }

    pub fn reset(&mut self) {
        self.frame_count = 0;
        self.feature_buffer_fill = 0;
        self.feature_buffer.fill(0.0);
        self.gru_a_condition.fill(0.0);
        self.gru_b_condition.fill(0.0);
        self.lpc = [0.0; LPC_ORDER];
        self.last_sig = [0.0; LPC_ORDER];
        self.last_exc = lin2ulaw(0.0);
        self.deemph_mem = 0.0;
        self.old_lpc = [[0.0; LPC_ORDER]; FEATURES_DELAY];
        self.nnet.gru_a_state.fill(0.0);
        self.nnet.gru_b_state.fill(0.0);
        self.rng.srand(b"LPCNet");
    }

    pub fn reset_signal(&mut self) {
        self.deemph_mem = 0.0;
        self.last_exc = lin2ulaw(0.0);
        self.last_sig = [0.0; LPC_ORDER];
        self.nnet.gru_a_state.fill(0.0);
        self.nnet.gru_b_state.fill(0.0);
    }

    /// Parse-only stub. Returns `Ok` (0) when the blob is structurally
    /// valid, `Err` (-1) otherwise. The arrays are discarded and
    /// `self.model` stays at its default (zero) values.
    ///
    /// **Population is Stage 7b.1.5 scope.** The C reference's generated
    /// `lpcnet_load_model` maps each weight record by name onto
    /// `self.model.*` fields (feature_conv1, gru_a_*, mdense, ...) —
    /// porting that mapping is mechanical but out of scope for 7b.1,
    /// which ships the plumbing + honest gap. Callers must gate on a
    /// `loaded` flag maintained at the enclosing state (e.g.
    /// `LPCNetPLCState.loaded`) rather than treating `Ok(0)` as
    /// "weights are live".
    pub fn load_model(&mut self, data: &[u8]) -> i32 {
        match parse_weights(data) {
            Ok(_list) => 0,
            Err(_) => -1,
        }
    }

    pub fn run_frame_network(&mut self, features: &[f32]) {
        let embed_dim = self.model.embed_pitch.dim;
        let conv1_out_size = self.model.feature_conv1.input_weights.nb_outputs;
        let conv2_out_size = self.model.feature_conv2.input_weights.nb_outputs;
        let dense1_out_size = self.model.feature_dense1.nb_outputs;
        let dense2_out_size = self.model.feature_dense2.nb_outputs;
        let gru_a_cond_size = self.model.gru_a_dense_feature.nb_outputs;
        let gru_b_cond_size = self.model.gru_b_dense_feature.nb_outputs;

        // Pitch embedding lookup.
        let pitch = (0.1 + 50.0 * features[NB_BANDS] + 100.0).floor() as i32;
        let pitch = pitch.max(33).min(255) as usize;

        let frame_input_size = NB_FEATURES + embed_dim;
        let mut frame_in = vec![0.0f32; frame_input_size];
        frame_in[..NB_FEATURES].copy_from_slice(&features[..NB_FEATURES]);
        self.model
            .embed_pitch
            .compute(&mut frame_in[NB_FEATURES..], pitch);

        // Feature conditioning network.
        let mut conv1_out = vec![0.0f32; conv1_out_size.max(1)];
        compute_conv1d_layer(
            &self.model.feature_conv1,
            &mut conv1_out,
            &mut self.nnet.feature_conv1_state,
            &frame_in,
        );
        if self.frame_count < self.model.feature_conv1.kernel_size as i32 - 1 {
            conv1_out.fill(0.0);
        }

        let mut conv2_out = vec![0.0f32; conv2_out_size.max(1)];
        compute_conv1d_layer(
            &self.model.feature_conv2,
            &mut conv2_out,
            &mut self.nnet.feature_conv2_state,
            &conv1_out,
        );
        if (self.frame_count as usize) < FEATURES_DELAY {
            conv2_out.fill(0.0);
        }

        let mut dense1_out = vec![0.0f32; dense1_out_size.max(1)];
        lpcnet_compute_dense(&self.model.feature_dense1, &mut dense1_out, &conv2_out);

        let mut condition = vec![0.0f32; dense2_out_size.max(1)];
        lpcnet_compute_dense(&self.model.feature_dense2, &mut condition, &dense1_out);

        // GRU conditioning.
        self.gru_a_condition.resize(gru_a_cond_size, 0.0);
        lpcnet_compute_dense(
            &self.model.gru_a_dense_feature,
            &mut self.gru_a_condition,
            &condition,
        );
        self.gru_b_condition.resize(gru_b_cond_size, 0.0);
        lpcnet_compute_dense(
            &self.model.gru_b_dense_feature,
            &mut self.gru_b_condition,
            &condition,
        );

        // LPC from cepstrum with delay line.
        if FEATURES_DELAY > 0 {
            self.lpc.copy_from_slice(&self.old_lpc[FEATURES_DELAY - 1]);
            for d in (1..FEATURES_DELAY).rev() {
                self.old_lpc[d] = self.old_lpc[d - 1];
            }
            lpc_from_cepstrum(&mut self.old_lpc[0], features);
        } else {
            lpc_from_cepstrum(&mut self.lpc, features);
        }

        if self.frame_count < 1000 {
            self.frame_count += 1;
        }
    }

    pub fn run_frame_network_deferred(&mut self, features: &[f32]) {
        let max_buf = self.model.feature_conv1.kernel_size + self.model.feature_conv2.kernel_size;
        let max_buf = max_buf.saturating_sub(2);
        let max_buf = max_buf.min(MAX_FEATURE_BUFFER_SIZE);
        if self.feature_buffer_fill == max_buf as i32 {
            let src = NB_FEATURES;
            let len = (max_buf - 1) * NB_FEATURES;
            self.feature_buffer.copy_within(src..src + len, 0);
        } else {
            self.feature_buffer_fill += 1;
        }
        let offset = (self.feature_buffer_fill as usize - 1) * NB_FEATURES;
        self.feature_buffer[offset..offset + NB_FEATURES].copy_from_slice(&features[..NB_FEATURES]);
    }

    pub fn run_frame_network_flush(&mut self) {
        let fill = self.feature_buffer_fill as usize;
        for i in 0..fill {
            let offset = i * NB_FEATURES;
            let features: Vec<f32> = self.feature_buffer[offset..offset + NB_FEATURES].to_vec();
            self.run_frame_network(&features);
        }
        self.feature_buffer_fill = 0;
    }

    fn run_sample_network(&mut self, last_exc: i32, last_sig_ulaw: i32, pred_ulaw: i32) -> i32 {
        let gru_a_size = self.nnet.gru_a_state.len();
        if gru_a_size == 0 {
            return 128;
        }
        let mut gru_a_input = vec![0.0f32; 3 * gru_a_size];
        compute_gru_a_input(
            &mut gru_a_input,
            &self.gru_a_condition,
            gru_a_size,
            &self.model.gru_a_embed_sig,
            last_sig_ulaw as usize,
            &self.model.gru_a_embed_pred,
            pred_ulaw as usize,
            &self.model.gru_a_embed_exc,
            last_exc as usize,
        );
        compute_sparse_gru(
            &self.model.sparse_gru_a,
            &mut self.nnet.gru_a_state,
            &gru_a_input,
        );
        let gru_b_size = self.nnet.gru_b_state.len();
        let in_b: Vec<f32> = self.nnet.gru_a_state.clone();
        let mut gru_b_input = self.gru_b_condition.clone();
        gru_b_input.resize(3 * gru_b_size, 0.0);
        compute_gru_b(
            &self.model.gru_b_recurrent,
            &self.model.gru_b_input_dense,
            &mut gru_b_input,
            &mut self.nnet.gru_b_state,
            &in_b,
        );
        sample_mdense(
            &self.model.dual_fc,
            &self.nnet.gru_b_state,
            &self.sampling_logit_table,
            &mut self.rng,
        )
    }

    pub fn synthesize_tail_impl(&mut self, output: &mut [i16], n: usize, preload: usize) {
        if self.frame_count <= FEATURES_DELAY as i32 {
            for s in output[..n].iter_mut() {
                *s = 0;
            }
            return;
        }
        for i in 0..n {
            let mut pred = 0.0f32;
            for j in 0..LPC_ORDER {
                pred -= self.last_sig[j] * self.lpc[j];
            }
            let last_sig_ulaw = lin2ulaw(self.last_sig[0]);
            let pred_ulaw = lin2ulaw(pred);
            let exc = self.run_sample_network(self.last_exc, last_sig_ulaw, pred_ulaw);
            let (exc_val, pcm_val);
            if i < preload {
                let out_f = output[i] as f32;
                exc_val = lin2ulaw(out_f - PREEMPH * self.deemph_mem - pred);
                pcm_val = out_f - PREEMPH * self.deemph_mem;
            } else {
                exc_val = exc;
                pcm_val = pred + ulaw2lin(exc as f32);
            }
            // Shift signal history right by 1.
            self.last_sig.copy_within(..LPC_ORDER - 1, 1);
            self.last_sig[0] = pcm_val;
            self.last_exc = exc_val;
            let pcm_out = pcm_val + PREEMPH * self.deemph_mem;
            self.deemph_mem = pcm_out;
            // Asymmetric clip [-32767, 32767] with rounding.
            let clipped = if pcm_out < -32767.0 {
                -32767.0
            } else if pcm_out > 32767.0 {
                32767.0
            } else {
                pcm_out
            };
            if i >= preload {
                output[i] = (0.5 + clipped).floor() as i16;
            }
        }
    }

    pub fn synthesize_impl(
        &mut self,
        features: &[f32],
        output: &mut [i16],
        n: usize,
        preload: usize,
    ) {
        self.run_frame_network(features);
        self.synthesize_tail_impl(output, n, preload);
    }

    pub fn synthesize(&mut self, features: &[f32], output: &mut [i16], n: usize) {
        self.synthesize_impl(features, output, n, 0);
    }

    pub fn synthesize_blend(&mut self, pcm_in: &[i16], output: &mut [i16], n: usize) {
        output[..n].copy_from_slice(&pcm_in[..n]);
        self.synthesize_tail_impl(output, n, n);
    }
}

// ===========================================================================
// LPCNetPLCState — Packet Loss Concealment (matches lpcnet_plc.c)
// ===========================================================================

#[derive(Clone, Debug)]
pub struct LPCNetPLCState {
    pub model: PLCModel,
    pub fargan: FARGANState,
    pub enc: LPCNetEncState,
    pub loaded: bool,
    pub arch: i32,
    // Reset-start marker: everything below cleared on reset.
    pub fec: Vec<[f32; NB_FEATURES]>,
    pub analysis_gap: bool,
    pub fec_read_pos: usize,
    pub fec_fill_pos: usize,
    pub fec_skip: usize,
    pub analysis_pos: usize,
    pub predict_pos: usize,
    pub pcm: Vec<f32>,
    pub blend: i32,
    pub features: [f32; NB_TOTAL_FEATURES],
    pub cont_features: Vec<f32>,
    pub loss_count: i32,
    pub plc_net: PLCNetState,
    pub plc_bak: [PLCNetState; 2],
}

impl Default for LPCNetPLCState {
    fn default() -> Self {
        Self {
            model: PLCModel::default(),
            fargan: FARGANState::default(),
            enc: LPCNetEncState::default(),
            loaded: false,
            arch: 0,
            fec: vec![[0.0; NB_FEATURES]; PLC_MAX_FEC],
            analysis_gap: true,
            fec_read_pos: 0,
            fec_fill_pos: 0,
            fec_skip: 0,
            analysis_pos: PLC_BUF_SIZE,
            predict_pos: PLC_BUF_SIZE,
            pcm: vec![0.0; PLC_BUF_SIZE],
            blend: 0,
            features: [0.0; NB_TOTAL_FEATURES],
            cont_features: vec![0.0; CONT_VECTORS * NB_FEATURES],
            loss_count: 0,
            plc_net: PLCNetState::default(),
            plc_bak: [PLCNetState::default(), PLCNetState::default()],
        }
    }
}

impl LPCNetPLCState {
    pub fn new() -> Self {
        let mut st = Self::default();
        st.init();
        st
    }

    pub fn init(&mut self) {
        self.fargan.init();
        self.enc = LPCNetEncState::new();
        self.loaded = false;
        self.reset();
    }

    /// Drop the per-stream runtime history, preserving loaded model
    /// weights and the `loaded` gate.
    ///
    /// Mirrors C `lpcnet_plc_reset` (`reference/dnn/lpcnet_plc.c:45`),
    /// which `OPUS_CLEAR`s from `LPCNET_PLC_RESET_START` onwards —
    /// the `fec` array. Fields *before* that marker in the C struct
    /// (`reference/dnn/lpcnet_private.h:50-71`) — `model`, `fargan`,
    /// `enc`, `loaded`, `arch` — survive the reset. C then explicitly
    /// re-initialises `enc` via `lpcnet_encoder_init` (which preserves
    /// PitchDNN weights but re-zeros encoder history buffers) and
    /// zero-fills `pcm` / `blend` / `loss_count` / `analysis_gap` /
    /// `analysis_pos` / `predict_pos` to their post-init values.
    ///
    /// Semantic consequence: a user-supplied `set_dnn_blob(custom)`
    /// followed by `reset()` keeps the custom weights live. The
    /// previous implementation wiped weights and re-applied embedded
    /// defaults, which silently reverted the user's override.
    pub fn reset(&mut self) {
        // Zero encoder runtime state while keeping its PitchDNN
        // weights. Rust doesn't have a `LPCNET_PLC_RESET_START`
        // marker, but `LPCNetEncState::new()` intentionally preserves
        // the `pitchdnn` field structure: the enclosing `PitchDNNState`
        // stays loaded because we keep `self.enc.pitchdnn` across this
        // call. We only overwrite the analysis/FFT buffers.
        let preserved_pitchdnn = std::mem::take(&mut self.enc.pitchdnn);
        self.enc = LPCNetEncState::new();
        self.enc.pitchdnn = preserved_pitchdnn;

        // `self.model`, `self.fargan`, `self.loaded`, `self.arch` are
        // intentionally NOT touched here — they sit before
        // `LPCNET_PLC_RESET_START` in the C layout.

        // Runtime history — everything from `fec` onwards in the C
        // struct.
        self.pcm.fill(0.0);
        self.blend = 0;
        self.loss_count = 0;
        self.analysis_gap = true;
        self.analysis_pos = PLC_BUF_SIZE;
        self.predict_pos = PLC_BUF_SIZE;
        self.fec_read_pos = 0;
        self.fec_fill_pos = 0;
        self.fec_skip = 0;
        self.features = [0.0; NB_TOTAL_FEATURES];
        self.cont_features.fill(0.0);
        for fec_row in self.fec.iter_mut() {
            fec_row.fill(0.0);
        }
        self.plc_net = PLCNetState::default();
        self.plc_bak = [PLCNetState::default(), PLCNetState::default()];
    }

    /// Parse the weight blob and populate every sub-model required for
    /// the neural PLC path.
    ///
    /// Mirrors C `lpcnet_plc_load_model` (`reference/dnn/lpcnet_plc.c:75`):
    ///
    /// 1. `init_plcmodel` — populates `self.model` (6 linear layers for
    ///    the PLC prediction GRUs and dense layers) from
    ///    `plcmodel_arrays`-style records.
    /// 2. `lpcnet_encoder_load_model` — delegates to
    ///    `pitchdnn_load_model` to populate the neural pitch estimator.
    /// 3. `fargan_load_model` — populates the FARGAN synthesis model.
    ///
    /// `self.loaded = true` is set **only** when all three succeed, so
    /// the SILK / CELT neural PLC path (which gates on `loaded`) runs
    /// with a fully populated state or not at all. A partial success
    /// leaves `loaded = false` and the caller falls through to the
    /// classical pitch/noise PLC on lost frames.
    ///
    /// Returns 0 on full success, non-zero on parse or population
    /// failure. The error code matches the step that failed, so a
    /// missing FARGAN record (for example) surfaces as -1 from
    /// `fargan.load_model`, not swallowed by an upstream step.
    ///
    /// Unknown / unused weight records are accepted silently (matches
    /// C semantics): the live tarball blob bundles DRED and OSCE
    /// weights we don't yet consume, and rejecting them would break
    /// the runtime path for no benefit.
    pub fn load_model(&mut self, data: &[u8]) -> i32 {
        // Parse the blob once so every sub-model sees the same arrays
        // without paying the parse cost three times. Keeps behaviour
        // identical to the C reference, which calls `parse_weights`
        // once in `lpcnet_plc_load_model`.
        let arrays = match parse_weights(data) {
            Ok(a) => a,
            Err(e) => {
                // Parse failure — we haven't mutated state yet, so the
                // previously-loaded weights stay live. Also clear the
                // gate: the caller's intent was to swap weights, and
                // leaving `loaded=true` on a failed swap would be a
                // lie about which weights are active.
                self.loaded = false;
                return e;
            }
        };

        // Atomic-load: build all three sub-models into locals first;
        // only commit to `self.*` after every build succeeds. This
        // prevents a "chimera" state where (e.g.) new PLC layers
        // coexist with stale FARGAN weights after a mid-load failure.
        let new_plc_model = match init_plcmodel(&arrays) {
            Ok(m) => m,
            Err(()) => {
                self.loaded = false;
                return -1;
            }
        };
        let new_pitchdnn_model = match PitchDNNState::try_init(&arrays) {
            Ok(m) => m,
            Err(()) => {
                self.loaded = false;
                return -1;
            }
        };
        let new_fargan_model = match FARGANState::try_init(&arrays) {
            Ok(m) => m,
            Err(()) => {
                self.loaded = false;
                return -1;
            }
        };

        // Commit point — past here, `self` is fully-populated with a
        // consistent set of weights. Nothing can fail between the
        // three commits (they are infallible moves).
        self.model = new_plc_model;
        self.enc.pitchdnn.apply_loaded_model(new_pitchdnn_model);
        self.fargan.apply_loaded_model(new_fargan_model);
        self.loaded = true;
        0
    }

    pub fn fec_add(&mut self, features: Option<&[f32]>) {
        match features {
            None => {
                self.fec_skip += 1;
            }
            Some(f) => {
                debug_assert!(self.fec_fill_pos < PLC_MAX_FEC);
                self.fec[self.fec_fill_pos][..NB_FEATURES].copy_from_slice(&f[..NB_FEATURES]);
                self.fec_fill_pos += 1;
            }
        }
    }

    pub fn fec_clear(&mut self) {
        self.fec_read_pos = 0;
        self.fec_fill_pos = 0;
        self.fec_skip = 0;
    }

    fn compute_plc_pred(&mut self, out: &mut [f32], input: &[f32]) {
        debug_assert!(self.loaded);
        let mut tmp = vec![0.0f32; self.model.plc_dense_in.nb_outputs.max(1)];
        compute_generic_dense(&self.model.plc_dense_in, &mut tmp, input, ACTIVATION_TANH);
        compute_generic_gru(
            &self.model.plc_gru1_input,
            &self.model.plc_gru1_recurrent,
            &mut self.plc_net.gru1_state,
            &tmp,
        );
        compute_generic_gru(
            &self.model.plc_gru2_input,
            &self.model.plc_gru2_recurrent,
            &mut self.plc_net.gru2_state,
            &self.plc_net.gru1_state.clone(),
        );
        compute_generic_dense(
            &self.model.plc_dense_out,
            out,
            &self.plc_net.gru2_state,
            ACTIVATION_LINEAR,
        );
    }

    fn get_fec_or_pred(&mut self, out: &mut [f32]) -> bool {
        if self.fec_read_pos != self.fec_fill_pos && self.fec_skip == 0 {
            let mut plc_features = vec![0.0f32; 2 * NB_BANDS + NB_FEATURES + 1];
            out[..NB_FEATURES].copy_from_slice(&self.fec[self.fec_read_pos]);
            self.fec_read_pos += 1;
            plc_features[2 * NB_BANDS..2 * NB_BANDS + NB_FEATURES]
                .copy_from_slice(&out[..NB_FEATURES]);
            plc_features[2 * NB_BANDS + NB_FEATURES] = -1.0;
            let mut discard = vec![0.0f32; NB_FEATURES];
            self.compute_plc_pred(&mut discard, &plc_features);
            true
        } else {
            let zeros = vec![0.0f32; 2 * NB_BANDS + NB_FEATURES + 1];
            self.compute_plc_pred(out, &zeros);
            if self.fec_skip > 0 {
                self.fec_skip -= 1;
            }
            false
        }
    }

    fn queue_features(&mut self, features: &[f32]) {
        self.cont_features
            .copy_within(NB_FEATURES..CONT_VECTORS * NB_FEATURES, 0);
        let start = (CONT_VECTORS - 1) * NB_FEATURES;
        self.cont_features[start..start + NB_FEATURES].copy_from_slice(&features[..NB_FEATURES]);
    }

    pub fn update(&mut self, pcm: &[i16]) -> i32 {
        if self.analysis_pos >= FRAME_SIZE {
            self.analysis_pos -= FRAME_SIZE;
        } else {
            self.analysis_gap = true;
        }
        if self.predict_pos >= FRAME_SIZE {
            self.predict_pos -= FRAME_SIZE;
        }
        self.pcm.copy_within(FRAME_SIZE..PLC_BUF_SIZE, 0);
        for i in 0..FRAME_SIZE {
            self.pcm[PLC_BUF_SIZE - FRAME_SIZE + i] = pcm[i] as f32 / 32768.0;
        }
        self.loss_count = 0;
        self.blend = 0;
        0
    }

    pub fn conceal(&mut self, pcm: &mut [i16]) -> i32 {
        debug_assert!(self.loaded);
        if self.blend == 0 {
            let mut count = 0;
            self.plc_net = self.plc_bak[0].clone();
            while self.analysis_pos + FRAME_SIZE <= PLC_BUF_SIZE {
                let mut x = [0.0f32; FRAME_SIZE];
                debug_assert!(self.analysis_pos < PLC_BUF_SIZE);
                for i in 0..FRAME_SIZE {
                    x[i] = 32768.0 * self.pcm[self.analysis_pos + i];
                }
                let mut plc_features = vec![0.0f32; 2 * NB_BANDS + NB_FEATURES + 1];
                burg_cepstral_analysis(&mut plc_features, &x);
                self.enc
                    .compute_single_frame_features_float(&x, &mut self.features);
                if (!self.analysis_gap || count > 0) && self.analysis_pos >= self.predict_pos {
                    self.queue_features(&self.features.clone());
                    plc_features[2 * NB_BANDS..2 * NB_BANDS + NB_FEATURES]
                        .copy_from_slice(&self.features[..NB_FEATURES]);
                    plc_features[2 * NB_BANDS + NB_FEATURES] = 1.0;
                    self.plc_bak[0] = self.plc_bak[1].clone();
                    self.plc_bak[1] = self.plc_net.clone();
                    let mut pred_out = vec![0.0f32; NB_FEATURES];
                    self.compute_plc_pred(&mut pred_out, &plc_features);
                    self.features[..NB_FEATURES].copy_from_slice(&pred_out);
                }
                self.analysis_pos += FRAME_SIZE;
                count += 1;
            }
            // Two prediction steps for lookahead.
            self.plc_bak[0] = self.plc_bak[1].clone();
            self.plc_bak[1] = self.plc_net.clone();
            let mut pred = vec![0.0f32; NB_FEATURES];
            self.get_fec_or_pred(&mut pred);
            self.features[..NB_FEATURES].copy_from_slice(&pred);
            self.queue_features(&self.features.clone());

            self.plc_bak[0] = self.plc_bak[1].clone();
            self.plc_bak[1] = self.plc_net.clone();
            let mut pred2 = vec![0.0f32; NB_FEATURES];
            self.get_fec_or_pred(&mut pred2);
            self.features[..NB_FEATURES].copy_from_slice(&pred2);
            self.queue_features(&self.features.clone());

            // Initialize FARGAN continuation.
            if PLC_BUF_SIZE >= FARGAN_CONT_SAMPLES {
                let start = PLC_BUF_SIZE - FARGAN_CONT_SAMPLES;
                let pcm_slice = self.pcm[start..PLC_BUF_SIZE].to_vec();
                let cont_feat = self.cont_features.clone();
                self.fargan.cont(&pcm_slice, &cont_feat);
            }
            self.analysis_gap = false;
        }

        // Common path for every lost frame.
        self.plc_bak[0] = self.plc_bak[1].clone();
        self.plc_bak[1] = self.plc_net.clone();
        let mut pred = vec![0.0f32; NB_FEATURES];
        let got_fec = self.get_fec_or_pred(&mut pred);
        self.features[..NB_FEATURES].copy_from_slice(&pred);
        if got_fec {
            self.loss_count = 0;
        } else {
            self.loss_count += 1;
        }

        // Energy attenuation.
        if self.loss_count >= 10 {
            self.features[0] = fmax(
                -15.0,
                self.features[0] + ATT_TABLE[9] - 2.0 * (self.loss_count - 9) as f32,
            );
        } else {
            self.features[0] = fmax(
                -15.0,
                self.features[0] + ATT_TABLE[self.loss_count as usize],
            );
        }

        let features_copy = self.features;
        self.fargan.synthesize_int(pcm, &features_copy);
        self.queue_features(&self.features.clone());

        if self.analysis_pos >= FRAME_SIZE {
            self.analysis_pos -= FRAME_SIZE;
        } else {
            self.analysis_gap = true;
        }
        self.predict_pos = PLC_BUF_SIZE;
        self.pcm.copy_within(FRAME_SIZE..PLC_BUF_SIZE, 0);
        for i in 0..FRAME_SIZE {
            self.pcm[PLC_BUF_SIZE - FRAME_SIZE + i] = pcm[i] as f32 / 32768.0;
        }
        self.blend = 1;
        0
    }
}

// ===========================================================================
// Test support (shared with other modules' tests)
// ===========================================================================

/// Synthetic weight blobs used by unit tests in this crate. Kept
/// inside the crate behind `#[cfg(test)]` — real weights live in
/// `reference/dnn/*_data.c` and are loaded by the harness.
#[cfg(test)]
pub(crate) mod test_support {
    use crate::dnn::core;

    fn append_weight_record(blob: &mut Vec<u8>, name: &str, weight_type: i32, payload_len: usize) {
        let mut record = vec![0u8; core::WEIGHT_BLOCK_SIZE + payload_len];
        record[4..8].copy_from_slice(&core::WEIGHT_BLOB_VERSION.to_ne_bytes());
        record[8..12].copy_from_slice(&weight_type.to_ne_bytes());
        record[12..16].copy_from_slice(&(payload_len as i32).to_ne_bytes());
        record[16..20].copy_from_slice(&(payload_len as i32).to_ne_bytes());
        let name_bytes = name.as_bytes();
        record[20..20 + name_bytes.len()].copy_from_slice(name_bytes);
        blob.extend_from_slice(&record);
    }

    pub(crate) fn append_float_record(blob: &mut Vec<u8>, name: &str, count: usize) {
        append_weight_record(blob, name, core::WEIGHT_TYPE_FLOAT, count * 4);
    }

    fn append_int8_record(blob: &mut Vec<u8>, name: &str, count: usize) {
        append_weight_record(blob, name, core::WEIGHT_TYPE_INT8, count);
    }

    /// A synthetic (zeroed) PitchDNN weight blob — enough records for
    /// `init_pitchdnn` to consider the model loaded. Useful for tests
    /// that need a successful `load_model` without real learned weights.
    pub(crate) fn make_pitchdnn_weight_blob() -> Vec<u8> {
        let mut blob = Vec::new();

        for name in [
            "dense_if_upsampler_1_bias",
            "dense_if_upsampler_1_subias",
            "dense_if_upsampler_1_scale",
        ] {
            append_float_record(&mut blob, name, 64);
        }
        append_int8_record(&mut blob, "dense_if_upsampler_1_weights_int8", 88 * 64);

        for name in [
            "dense_if_upsampler_2_bias",
            "dense_if_upsampler_2_subias",
            "dense_if_upsampler_2_scale",
        ] {
            append_float_record(&mut blob, name, 64);
        }
        append_int8_record(&mut blob, "dense_if_upsampler_2_weights_int8", 64 * 64);

        append_float_record(&mut blob, "conv2d_1_bias", 4);
        append_float_record(&mut blob, "conv2d_1_weight_float", 4 * 3 * 3);

        append_float_record(&mut blob, "conv2d_2_bias", 1);
        append_float_record(&mut blob, "conv2d_2_weight_float", 4 * 3 * 3);

        for name in [
            "dense_downsampler_bias",
            "dense_downsampler_subias",
            "dense_downsampler_scale",
        ] {
            append_float_record(&mut blob, name, 64);
        }
        append_int8_record(&mut blob, "dense_downsampler_weights_int8", 288 * 64);

        for name in [
            "gru_1_input_bias",
            "gru_1_input_subias",
            "gru_1_input_scale",
        ] {
            append_float_record(&mut blob, name, 3 * 64);
        }
        append_int8_record(&mut blob, "gru_1_input_weights_int8", 64 * 3 * 64);

        for name in [
            "gru_1_recurrent_bias",
            "gru_1_recurrent_subias",
            "gru_1_recurrent_scale",
        ] {
            append_float_record(&mut blob, name, 3 * 64);
        }
        append_int8_record(&mut blob, "gru_1_recurrent_weights_int8", 64 * 3 * 64);

        for name in [
            "dense_final_upsampler_bias",
            "dense_final_upsampler_subias",
            "dense_final_upsampler_scale",
        ] {
            append_float_record(&mut blob, name, 3 * 64);
        }
        append_int8_record(&mut blob, "dense_final_upsampler_weights_int8", 64 * 3 * 64);

        blob
    }

    fn append_fargan_zero_weight_records(blob: &mut Vec<u8>) {
        // Conditioning network.
        append_float_record(blob, "cond_net_pembed_bias", 12);
        append_float_record(blob, "cond_net_pembed_weights_float", 224 * 12);
        append_float_record(blob, "cond_net_fdense1_bias", 64);
        append_float_record(blob, "cond_net_fconv1_bias", 128);
        append_int8_record(blob, "cond_net_fconv1_weights_int8", 64 * 3 * 128);
        append_float_record(blob, "cond_net_fconv1_subias", 128);
        append_float_record(blob, "cond_net_fconv1_scale", 128);
        append_float_record(blob, "cond_net_fdense2_bias", 320);
        append_int8_record(blob, "cond_net_fdense2_weights_int8", 128 * 320);
        append_float_record(blob, "cond_net_fdense2_subias", 320);
        append_float_record(blob, "cond_net_fdense2_scale", 320);

        // Signal network.
        append_float_record(blob, "sig_net_cond_gain_dense_bias", 1);
        append_float_record(blob, "sig_net_fwc0_conv_bias", 192);
        append_int8_record(blob, "sig_net_fwc0_conv_weights_int8", 164 * 2 * 192);
        append_float_record(blob, "sig_net_fwc0_conv_subias", 192);
        append_float_record(blob, "sig_net_fwc0_conv_scale", 192);
        append_float_record(blob, "sig_net_fwc0_glu_gate_bias", 192);
        append_int8_record(blob, "sig_net_fwc0_glu_gate_weights_int8", 192 * 192);
        append_float_record(blob, "sig_net_fwc0_glu_gate_subias", 192);
        append_float_record(blob, "sig_net_fwc0_glu_gate_scale", 192);
        append_float_record(blob, "sig_net_gain_dense_out_bias", 4);
        append_float_record(blob, "sig_net_gain_dense_out_weights_float", 192 * 4);

        append_int8_record(
            blob,
            "sig_net_gru1_input_weights_int8",
            (192 + 80) * (3 * 160),
        );
        append_float_record(blob, "sig_net_gru1_input_subias", 3 * 160);
        append_float_record(blob, "sig_net_gru1_input_scale", 3 * 160);
        append_int8_record(blob, "sig_net_gru1_recurrent_weights_int8", 160 * (3 * 160));
        append_float_record(blob, "sig_net_gru1_recurrent_subias", 3 * 160);
        append_float_record(blob, "sig_net_gru1_recurrent_scale", 3 * 160);
        append_int8_record(blob, "sig_net_gru1_glu_gate_weights_int8", 160 * 160);
        append_float_record(blob, "sig_net_gru1_glu_gate_bias", 160);
        append_float_record(blob, "sig_net_gru1_glu_gate_subias", 160);
        append_float_record(blob, "sig_net_gru1_glu_gate_scale", 160);

        append_int8_record(
            blob,
            "sig_net_gru2_input_weights_int8",
            (160 + 80) * (3 * 128),
        );
        append_float_record(blob, "sig_net_gru2_input_subias", 3 * 128);
        append_float_record(blob, "sig_net_gru2_input_scale", 3 * 128);
        append_int8_record(blob, "sig_net_gru2_recurrent_weights_int8", 128 * (3 * 128));
        append_float_record(blob, "sig_net_gru2_recurrent_subias", 3 * 128);
        append_float_record(blob, "sig_net_gru2_recurrent_scale", 3 * 128);
        append_int8_record(blob, "sig_net_gru2_glu_gate_weights_int8", 128 * 128);
        append_float_record(blob, "sig_net_gru2_glu_gate_bias", 128);
        append_float_record(blob, "sig_net_gru2_glu_gate_subias", 128);
        append_float_record(blob, "sig_net_gru2_glu_gate_scale", 128);

        append_int8_record(
            blob,
            "sig_net_gru3_input_weights_int8",
            (128 + 80) * (3 * 128),
        );
        append_float_record(blob, "sig_net_gru3_input_subias", 3 * 128);
        append_float_record(blob, "sig_net_gru3_input_scale", 3 * 128);
        append_int8_record(blob, "sig_net_gru3_recurrent_weights_int8", 128 * (3 * 128));
        append_float_record(blob, "sig_net_gru3_recurrent_subias", 3 * 128);
        append_float_record(blob, "sig_net_gru3_recurrent_scale", 3 * 128);
        append_int8_record(blob, "sig_net_gru3_glu_gate_weights_int8", 128 * 128);
        append_float_record(blob, "sig_net_gru3_glu_gate_bias", 128);
        append_float_record(blob, "sig_net_gru3_glu_gate_subias", 128);
        append_float_record(blob, "sig_net_gru3_glu_gate_scale", 128);

        append_float_record(blob, "sig_net_skip_dense_bias", 128);
        append_int8_record(blob, "sig_net_skip_dense_weights_int8", 688 * 128);
        append_float_record(blob, "sig_net_skip_dense_subias", 128);
        append_float_record(blob, "sig_net_skip_dense_scale", 128);
        append_float_record(blob, "sig_net_skip_glu_gate_bias", 128);
        append_int8_record(blob, "sig_net_skip_glu_gate_weights_int8", 128 * 128);
        append_float_record(blob, "sig_net_skip_glu_gate_subias", 128);
        append_float_record(blob, "sig_net_skip_glu_gate_scale", 128);

        append_int8_record(blob, "sig_net_sig_dense_out_weights_int8", 128 * 40);
        append_float_record(blob, "sig_net_sig_dense_out_bias", 40);
        append_float_record(blob, "sig_net_sig_dense_out_subias", 40);
        append_float_record(blob, "sig_net_sig_dense_out_scale", 40);
    }

    /// Append synthetic (zeroed) PLC-model weight records in the order
    /// expected by `init_plcmodel`. Sizes match the dimensions in
    /// `reference/dnn/plc_data.c` (57→128 dense, 128/192/192→576 GRUs,
    /// 192→20 output dense).
    fn append_plc_model_zero_weight_records(blob: &mut Vec<u8>) {
        // plc_dense_in: Linear(57 -> 128), float-only.
        append_float_record(blob, "plc_dense_in_bias", 128);
        append_float_record(blob, "plc_dense_in_weights_float", 57 * 128);

        // plc_dense_out: Linear(192 -> 20), float-only.
        append_float_record(blob, "plc_dense_out_bias", 20);
        append_float_record(blob, "plc_dense_out_weights_float", 192 * 20);

        // plc_gru1_input: Linear(128 -> 576), quantized with bias.
        append_float_record(blob, "plc_gru1_input_bias", 576);
        append_int8_record(blob, "plc_gru1_input_weights_int8", 128 * 576);
        append_float_record(blob, "plc_gru1_input_subias", 576);
        append_float_record(blob, "plc_gru1_input_scale", 576);

        // plc_gru1_recurrent: Linear(192 -> 576), quantized with bias.
        append_float_record(blob, "plc_gru1_recurrent_bias", 576);
        append_int8_record(blob, "plc_gru1_recurrent_weights_int8", 192 * 576);
        append_float_record(blob, "plc_gru1_recurrent_subias", 576);
        append_float_record(blob, "plc_gru1_recurrent_scale", 576);

        // plc_gru2_input: Linear(192 -> 576), quantized with bias.
        append_float_record(blob, "plc_gru2_input_bias", 576);
        append_int8_record(blob, "plc_gru2_input_weights_int8", 192 * 576);
        append_float_record(blob, "plc_gru2_input_subias", 576);
        append_float_record(blob, "plc_gru2_input_scale", 576);

        // plc_gru2_recurrent: Linear(192 -> 576), quantized with bias.
        append_float_record(blob, "plc_gru2_recurrent_bias", 576);
        append_int8_record(blob, "plc_gru2_recurrent_weights_int8", 192 * 576);
        append_float_record(blob, "plc_gru2_recurrent_subias", 576);
        append_float_record(blob, "plc_gru2_recurrent_scale", 576);
    }

    /// A full PLC weight blob combining PitchDNN + FARGAN + PLC-model
    /// records so `LPCNetPLCState::load_model` returns `Ok(())` and
    /// `loaded` flips to `true`.
    pub(crate) fn make_plc_weight_blob() -> Vec<u8> {
        let mut blob = make_pitchdnn_weight_blob();
        append_fargan_zero_weight_records(&mut blob);
        append_plc_model_zero_weight_records(&mut blob);
        blob
    }
}

// ===========================================================================
// Unit tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;

    fn patterned_pcm(seed: i32) -> [i16; FRAME_SIZE] {
        let mut pcm = [0i16; FRAME_SIZE];
        for (i, sample) in pcm.iter_mut().enumerate() {
            let v = ((i as i32 * 137 + seed * 29) % 32000) - 16000;
            *sample = v as i16;
        }
        pcm
    }

    fn patterned_frame(seed: i32) -> [f32; FRAME_SIZE] {
        let pcm = patterned_pcm(seed);
        let mut input = [0.0f32; FRAME_SIZE];
        for (dst, src) in input.iter_mut().zip(pcm) {
            *dst = src as f32 / 32768.0;
        }
        input
    }

    fn zero_linear(nb_inputs: usize, nb_outputs: usize) -> LinearLayer {
        LinearLayer {
            nb_inputs,
            nb_outputs,
            ..Default::default()
        }
    }

    // Reuse the blob builders from the sibling `test_support` module so
    // there is one source of truth for the synthetic weights layout.
    // `append_float_record` is re-exported for tests that write lone ad-hoc
    // records (e.g. a single "demo" float).
    use super::test_support::{
        append_float_record, make_pitchdnn_weight_blob, make_plc_weight_blob,
    };

    #[test]
    fn test_kiss99_srand_and_rand() {
        let mut rng = Kiss99Ctx::default();
        rng.srand(b"LPCNet");
        // First few outputs should be deterministic.
        let v0 = rng.rand();
        let v1 = rng.rand();
        assert_ne!(v0, v1);
        // Verify the seed state is non-zero.
        assert_ne!(rng.z, 0);
        assert_ne!(rng.w, 0);
        assert_ne!(rng.jsr, 0);
    }

    #[test]
    fn test_kiss99_srand_short_seeds_cover_tail_bytes() {
        let mut empty = Kiss99Ctx::default();
        empty.srand(&[]);

        let mut one = Kiss99Ctx::default();
        one.srand(b"A");
        assert_ne!(one.z, empty.z);
        assert_eq!(one.w, empty.w);
        assert_eq!(one.jsr, empty.jsr);

        let mut two = Kiss99Ctx::default();
        two.srand(b"AB");
        assert_ne!(two.z, empty.z);
        assert_ne!(two.w, empty.w);
        assert_eq!(two.jsr, empty.jsr);

        let mut three = Kiss99Ctx::default();
        three.srand(b"ABC");
        assert_ne!(three.z, empty.z);
        assert_ne!(three.w, empty.w);
        assert_ne!(three.jsr, empty.jsr);
    }

    #[test]
    fn test_fft_roundtrip() {
        let mut x = [0.0f32; WINDOW_SIZE];
        x[0] = 1.0;
        x[10] = 0.5;
        x[100] = -0.3;
        let mut spectrum = vec![Cpx::default(); FREQ_SIZE];
        forward_transform(&mut spectrum, &x);
        let mut x_full = [Cpx::default(); FREQ_SIZE];
        x_full.copy_from_slice(&spectrum);
        let mut recovered = [0.0f32; WINDOW_SIZE];
        inverse_transform(&mut recovered, &x_full);
        for i in 0..WINDOW_SIZE {
            assert!(
                (x[i] - recovered[i]).abs() < 1e-3,
                "FFT roundtrip mismatch at {}: {} vs {}",
                i,
                x[i],
                recovered[i]
            );
        }
    }

    #[test]
    fn test_dct_energy_preservation() {
        let input: [f32; NB_BANDS] = [
            1.0, 0.5, -0.3, 0.2, 0.8, -0.1, 0.0, 0.4, -0.6, 0.3, 0.7, -0.2, 0.1, -0.5, 0.9, 0.0,
            -0.4, 0.6,
        ];
        let mut output = [0.0f32; NB_BANDS];
        dct(&mut output, &input);
        let e_in: f32 = input.iter().map(|x| x * x).sum();
        let e_out: f32 = output.iter().map(|x| x * x).sum();
        // Parseval's theorem: energy should be approximately preserved.
        assert!(
            (e_in - e_out).abs() < 1e-3,
            "DCT energy: in={} out={}",
            e_in,
            e_out
        );
    }

    #[test]
    fn test_dct_idct_roundtrip() {
        let input: [f32; NB_BANDS] = [
            -2.0, 0.1, -0.5, 0.3, -0.1, 0.2, 0.0, -0.3, 0.4, -0.2, 0.6, -0.4, 0.5, -0.6, 0.7, -0.7,
            0.8, -0.8,
        ];
        let mut dct_out = [0.0f32; NB_BANDS];
        let mut recovered = [0.0f32; NB_BANDS];
        dct(&mut dct_out, &input);
        idct(&mut recovered, &dct_out);
        for i in 0..NB_BANDS {
            assert!(
                (input[i] - recovered[i]).abs() < 1e-4,
                "DCT roundtrip mismatch at {}: {} vs {}",
                i,
                input[i],
                recovered[i]
            );
        }
    }

    #[test]
    fn test_preemphasis() {
        let input = [1.0f32, 2.0, 3.0, 4.0];
        let mut output = [0.0f32; 4];
        let mut mem = 0.0f32;
        preemphasis(&mut output, &mut mem, &input, 0.85, 4);
        // y[0] = x[0] + mem(=0) = 1.0
        assert!((output[0] - 1.0).abs() < 1e-6);
        // y[1] = x[1] + (-0.85 * x[0]) = 2.0 - 0.85 = 1.15
        assert!((output[1] - 1.15).abs() < 1e-6);
    }

    #[test]
    fn test_burg_analysis_simple() {
        // White noise-like signal — Burg should return small coefficients.
        let x: Vec<f32> = (0..160)
            .map(|i| ((i * 7 + 13) % 37) as f32 / 37.0 - 0.5)
            .collect();
        let mut a = [0.0f32; LPC_ORDER];
        let g = silk_burg_analysis(&mut a, &x, 1e-3, 160, 1, LPC_ORDER);
        assert!(g >= 0.0, "Residual energy should be non-negative");
    }

    #[test]
    fn test_burg_analysis_zero_signal_hits_gain_limit() {
        let x = [0.0f32; FRAME_SIZE];
        let mut a = [1.0f32; LPC_ORDER];
        let g = silk_burg_analysis(&mut a, &x, 1.0, FRAME_SIZE, 1, LPC_ORDER);
        assert_eq!(g, 0.0);
        assert!(a.iter().all(|&v| v == 0.0));
    }

    #[test]
    fn test_burg_cepstral_analysis_identical_halves_have_small_delta() {
        let mut x = [0.0f32; FRAME_SIZE];
        for i in 0..FRAME_SIZE / 2 {
            let v = ((i * 19 + 7) % 67) as f32 / 67.0 - 0.5;
            x[i] = v;
            x[i + FRAME_SIZE / 2] = v;
        }

        let mut ceps = [0.0f32; 2 * NB_BANDS];
        burg_cepstral_analysis(&mut ceps, &x);

        assert!(ceps.iter().all(|v| v.is_finite()));
        for i in 0..NB_BANDS {
            assert!(ceps[NB_BANDS + i].abs() < 1e-4);
        }
    }

    #[test]
    fn test_burg_cepstral_analysis_produces_finite_split_features() {
        let x = patterned_frame(23);
        let mut ceps = [0.0f32; 2 * NB_BANDS];

        burg_cepstral_analysis(&mut ceps, &x);

        assert!(ceps.iter().all(|v| v.is_finite()));
        assert!(ceps[..NB_BANDS].iter().any(|&v| v != 0.0));
        assert!(ceps[NB_BANDS..].iter().any(|&v| v != 0.0));
    }

    #[test]
    fn test_lpcn_lpc_zero_input_and_early_break() {
        let mut lpc = [1.0f32; LPC_ORDER];
        let mut rc = [1.0f32; LPC_ORDER];
        let ac_zero = [0.0f32; LPC_ORDER + 1];
        assert_eq!(lpcn_lpc(&mut lpc, &mut rc, &ac_zero, LPC_ORDER), 0.0);
        assert!(lpc.iter().all(|&v| v == 0.0));
        assert!(rc.iter().all(|&v| v == 0.0));

        let mut lpc = [0.0f32; LPC_ORDER];
        let mut rc = [0.0f32; LPC_ORDER];
        let mut ac = [0.0f32; LPC_ORDER + 1];
        ac[0] = 1.0;
        ac[1] = 1.0;
        let error = lpcn_lpc(&mut lpc, &mut rc, &ac, LPC_ORDER);
        assert_eq!(error, 0.0);
        assert_eq!(lpc[0], -1.0);
        assert!(lpc[1..].iter().all(|&v| v == 0.0));
        assert_eq!(rc[0], -1.0);
        assert!(rc[1..].iter().all(|&v| v == 0.0));
    }

    #[test]
    fn test_compute_gru_helpers_cover_state_updates_and_embedding_paths() {
        let recurrent = zero_linear(2, 6);
        let input_dense = LinearLayer {
            bias: Some(vec![0.0, 0.0]),
            float_weights: Some(vec![1.0, 0.0, 0.0, 1.0]),
            nb_inputs: 2,
            nb_outputs: 2,
            ..Default::default()
        };

        let mut sparse_state = [0.25f32, -0.25];
        let sparse_input = [4.0f32, -4.0, 1.0, -1.0, 0.5, -0.5];
        compute_sparse_gru(&recurrent, &mut sparse_state, &sparse_input);
        assert!(sparse_state.iter().all(|v| v.is_finite()));
        assert_ne!(sparse_state, [0.25, -0.25]);

        let mut gru_input = vec![1.0f32, -1.0, 0.5, -0.5, -0.25, 0.25];
        let mut gru_state = vec![0.5f32, -0.5];
        let extra = [2.0f32, -3.0];
        compute_gru_b(
            &recurrent,
            &input_dense,
            &mut gru_input,
            &mut gru_state,
            &extra,
        );
        assert!(gru_state.iter().all(|v| v.is_finite()));
        assert_ne!(gru_state, vec![0.5, -0.5]);

        let embed_sig = EmbeddingLayer {
            weights: vec![10.0, 20.0, 30.0, 40.0],
            dim: 2,
        };
        let embed_pred = EmbeddingLayer {
            weights: vec![-1.0, -2.0, -3.0, -4.0],
            dim: 2,
        };
        let embed_exc = EmbeddingLayer {
            weights: vec![0.5, 1.5, 2.5, 3.5],
            dim: 2,
        };
        let condition = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
        let mut output = [0.0f32; 6];
        compute_gru_a_input(
            &mut output,
            &condition,
            2,
            &embed_sig,
            1,
            &embed_pred,
            0,
            &embed_exc,
            0,
        );
        assert_eq!(output, [30.5, 41.5, 3.0, 4.0, 5.0, 6.0]);
    }

    #[test]
    fn test_sample_mdense_covers_sampling_and_fallback_paths() {
        let layer = MDenseLayer {
            fc1: zero_linear(1, 256),
            fc2: zero_linear(1, 256),
        };
        let state = [0.0f32];
        let mut rng = Kiss99Ctx::default();
        rng.srand(b"LPCNet");

        let mut uniform_logits = [0.0f32; 256];
        let sample = sample_mdense(&layer, &state, &uniform_logits, &mut rng);
        assert!((0..=255).contains(&sample));

        uniform_logits.fill(-1000.0);
        let fallback = sample_mdense(&layer, &state, &uniform_logits, &mut rng);
        assert_eq!(fallback, 255);
    }

    #[test]
    fn test_compute_single_frame_feature_wrappers_match_for_loaded_pitchdnn() {
        let blob = make_pitchdnn_weight_blob();
        let pcm_i16 = patterned_pcm(31);
        let mut pcm_f32 = [0.0f32; FRAME_SIZE];
        for (dst, src) in pcm_f32.iter_mut().zip(pcm_i16) {
            *dst = src as f32;
        }

        let mut enc_i16 = LPCNetEncState::new();
        let mut enc_f32 = LPCNetEncState::new();
        assert_eq!(enc_i16.load_model(&blob), 0);
        assert_eq!(enc_f32.load_model(&blob), 0);

        let mut features_i16 = [0.0f32; NB_TOTAL_FEATURES];
        let mut features_f32 = [0.0f32; NB_TOTAL_FEATURES];
        assert_eq!(
            enc_i16.compute_single_frame_features(&pcm_i16, &mut features_i16),
            0
        );
        assert_eq!(
            enc_f32.compute_single_frame_features_float(&pcm_f32, &mut features_f32),
            0
        );

        for i in 0..NB_TOTAL_FEATURES {
            assert!(
                (features_i16[i] - features_f32[i]).abs() < 1e-4,
                "feature {} mismatch: {} vs {}",
                i,
                features_i16[i],
                features_f32[i]
            );
        }
        assert!(features_i16.iter().any(|&v| v != 0.0));
    }

    #[test]
    fn test_encoder_init() {
        let enc = LPCNetEncState::new();
        assert_eq!(enc.dnn_pitch, 0.0);
        assert_eq!(enc.mem_preemph, 0.0);
    }

    #[test]
    fn test_synthesizer_init() {
        let st = LPCNetState::new();
        assert_eq!(st.frame_count, 0);
        assert_eq!(st.last_exc, lin2ulaw(0.0));
        // Sampling logit table should be populated.
        assert!(st.sampling_logit_table[0] < 0.0); // logit(0.025) < 0
        assert!(st.sampling_logit_table[255] > 0.0); // logit(0.975) > 0
    }

    #[test]
    fn test_plc_init_and_reset() {
        let mut plc = LPCNetPLCState::new();
        assert!(plc.analysis_gap);
        assert_eq!(plc.analysis_pos, PLC_BUF_SIZE);
        plc.reset();
        assert_eq!(plc.blend, 0);
        assert_eq!(plc.loss_count, 0);
    }

    #[test]
    fn test_lpcnet_state_reset_signal_clears_runtime_state() {
        let mut st = LPCNetState::new();
        st.deemph_mem = 1.25;
        st.last_exc = 42;
        st.last_sig.fill(3.0);
        st.nnet.gru_a_state.fill(0.5);
        st.nnet.gru_b_state.fill(-0.5);

        st.reset_signal();

        assert_eq!(st.deemph_mem, 0.0);
        assert_eq!(st.last_exc, lin2ulaw(0.0));
        assert!(st.last_sig.iter().all(|&v| v == 0.0));
        assert!(st.nnet.gru_a_state.iter().all(|&v| v == 0.0));
        assert!(st.nnet.gru_b_state.iter().all(|&v| v == 0.0));
    }

    #[test]
    fn test_band_energy_symmetry() {
        // DC-only signal: all energy in band 0.
        let mut x = vec![Cpx::default(); FREQ_SIZE];
        x[0] = Cpx { r: 1.0, i: 0.0 };
        let mut e = [0.0f32; NB_BANDS];
        lpcn_compute_band_energy(&mut e, &x);
        assert!(e[0] > 0.0, "DC energy should be positive");
    }

    #[test]
    fn test_lpcnet_state_load_model_invalid_blob() {
        let mut st = LPCNetState::new();
        assert_eq!(st.load_model(&[1, 2, 3]), -1);
    }

    #[test]
    fn test_lpcnet_state_load_model_accepts_valid_blob() {
        let mut blob = Vec::new();
        append_float_record(&mut blob, "demo", 1);

        let mut st = LPCNetState::new();
        assert_eq!(st.load_model(&blob), 0);
    }

    #[test]
    fn test_plc_load_model_invalid_blob_returns_error() {
        let mut plc = LPCNetPLCState::new();
        assert_eq!(plc.load_model(&[1, 2, 3]), -1);
        assert!(!plc.loaded);
    }

    #[test]
    fn test_plc_load_model_accepts_encoder_blob_but_not_fargan_blob() {
        let blob = make_pitchdnn_weight_blob();

        let mut plc = LPCNetPLCState::new();
        assert_eq!(plc.load_model(&blob), -1);
        assert!(!plc.loaded);
    }

    #[test]
    fn test_run_frame_network_updates_state_and_deferred_flush() {
        let mut st = LPCNetState::new();
        st.model.feature_conv1.kernel_size = 2;
        st.model.feature_conv2.kernel_size = 2;
        let mut features = [0.0f32; NB_FEATURES];
        features[NB_BANDS] = 0.25;

        st.run_frame_network(&features);
        assert_eq!(st.frame_count, 1);
        assert!(st.lpc.iter().all(|x| x.is_finite()));
        assert!(st.old_lpc[0].iter().all(|x| x.is_finite()));

        st.run_frame_network(&features);
        assert_eq!(st.frame_count, 2);

        st.run_frame_network_deferred(&features);
        st.run_frame_network_deferred(&features);
        assert_eq!(st.feature_buffer_fill, 2);
        let mut shifted_features = features;
        shifted_features[0] = 1.0;
        st.run_frame_network_deferred(&shifted_features);
        assert_eq!(st.feature_buffer_fill, 2);
        assert_eq!(&st.feature_buffer[..NB_FEATURES], &features[..NB_FEATURES]);
        assert_eq!(
            &st.feature_buffer[NB_FEATURES..2 * NB_FEATURES],
            &shifted_features[..NB_FEATURES]
        );
        st.run_frame_network_flush();
        assert_eq!(st.feature_buffer_fill, 0);
        assert!(st.frame_count >= 3);
    }

    #[test]
    fn test_burg_cepstral_analysis_emits_finite_bands() {
        let x = patterned_frame(19);
        let mut ceps = [0.0f32; 2 * NB_BANDS];

        burg_cepstral_analysis(&mut ceps, &x);

        assert!(ceps.iter().all(|value| value.is_finite()));
        assert!(ceps.iter().any(|&value| value != 0.0));
    }

    #[test]
    fn test_recurrent_helpers_update_state_and_sample_edges() {
        let recurrent = LinearLayer {
            bias: Some(vec![0.5, -0.25, 0.75]),
            float_weights: Some(vec![0.0; 3]),
            nb_inputs: 1,
            nb_outputs: 3,
            ..Default::default()
        };
        let mut sparse_state = [0.25f32];
        let sparse_input = [0.1f32, -0.2, 0.3];
        compute_sparse_gru(&recurrent, &mut sparse_state, &sparse_input);
        assert!(sparse_state[0].is_finite());
        assert_ne!(sparse_state[0], 0.25);

        let input_dense = LinearLayer {
            bias: Some(vec![0.2]),
            float_weights: Some(vec![0.0]),
            nb_inputs: 1,
            nb_outputs: 1,
            ..Default::default()
        };
        let mut gru_input = vec![0.2f32, -0.1, 0.05];
        let mut gru_b_state = vec![0.1f32];
        compute_gru_b(
            &recurrent,
            &input_dense,
            &mut gru_input,
            &mut gru_b_state,
            &[0.4],
        );
        assert!(gru_b_state[0].is_finite());
        assert!(gru_input.iter().all(|value| value.is_finite()));

        let force_zero = MDenseLayer {
            fc1: LinearLayer {
                bias: Some(vec![-1000.0; 256]),
                nb_inputs: 0,
                nb_outputs: 256,
                ..Default::default()
            },
            fc2: LinearLayer {
                bias: Some(vec![-1000.0; 256]),
                nb_inputs: 0,
                nb_outputs: 256,
                ..Default::default()
            },
        };
        let mut rng = Kiss99Ctx::default();
        let fallback = sample_mdense(&force_zero, &[], &[-1000.0; 256], &mut rng);
        assert_eq!(fallback, 255);

        let force_first = MDenseLayer {
            fc1: LinearLayer {
                bias: Some(
                    std::iter::once(1000.0)
                        .chain(std::iter::repeat_n(-1000.0, 255))
                        .collect(),
                ),
                nb_inputs: 0,
                nb_outputs: 256,
                ..Default::default()
            },
            fc2: LinearLayer {
                bias: Some(
                    std::iter::once(1000.0)
                        .chain(std::iter::repeat_n(-1000.0, 255))
                        .collect(),
                ),
                nb_inputs: 0,
                nb_outputs: 256,
                ..Default::default()
            },
        };
        let mut rng = Kiss99Ctx::default();
        let sampled = sample_mdense(&force_first, &[], &[0.0; 256], &mut rng);
        assert_eq!(sampled, 0);
    }

    #[test]
    fn test_compute_single_frame_feature_wrappers_and_sample_network_paths() {
        let blob = make_pitchdnn_weight_blob();

        let mut enc = LPCNetEncState::new();
        assert_eq!(enc.load_model(&blob), 0);

        let pcm_i16 = patterned_pcm(23);
        let mut ceps_i16 = [0.0f32; NB_TOTAL_FEATURES];
        assert_eq!(
            enc.compute_single_frame_features(&pcm_i16, &mut ceps_i16),
            0
        );
        assert!(ceps_i16.iter().all(|value| value.is_finite()));

        let pcm_f32 = patterned_frame(29);
        let mut ceps_f32 = [0.0f32; NB_TOTAL_FEATURES];
        assert_eq!(
            enc.compute_single_frame_features_float(&pcm_f32, &mut ceps_f32),
            0
        );
        assert!(ceps_f32.iter().all(|value| value.is_finite()));

        let mut st = LPCNetState::new();
        assert_eq!(st.run_sample_network(0, 0, 0), 128);

        st.nnet.gru_a_state = vec![0.0];
        st.nnet.gru_b_state = vec![0.0];
        st.gru_a_condition = vec![0.0; 3];
        st.gru_b_condition = vec![0.0; 3];
        st.model.gru_a_embed_sig.dim = 1;
        st.model.gru_a_embed_sig.weights = vec![0.0];
        st.model.gru_a_embed_pred.dim = 1;
        st.model.gru_a_embed_pred.weights = vec![0.0];
        st.model.gru_a_embed_exc.dim = 1;
        st.model.gru_a_embed_exc.weights = vec![0.0];
        st.model.sparse_gru_a = LinearLayer {
            bias: Some(vec![1000.0, -1000.0, 1000.0]),
            nb_inputs: 1,
            nb_outputs: 3,
            ..Default::default()
        };
        st.model.gru_b_recurrent = LinearLayer {
            bias: Some(vec![1000.0, -1000.0, 1000.0]),
            nb_inputs: 1,
            nb_outputs: 3,
            ..Default::default()
        };
        st.model.gru_b_input_dense = LinearLayer {
            bias: Some(vec![0.0]),
            nb_inputs: 1,
            nb_outputs: 1,
            ..Default::default()
        };
        st.model.dual_fc = MDenseLayer {
            fc1: LinearLayer {
                bias: Some(
                    std::iter::once(1000.0)
                        .chain(std::iter::repeat_n(-1000.0, 255))
                        .collect(),
                ),
                nb_inputs: 1,
                nb_outputs: 256,
                ..Default::default()
            },
            fc2: LinearLayer {
                bias: Some(
                    std::iter::once(1000.0)
                        .chain(std::iter::repeat_n(-1000.0, 255))
                        .collect(),
                ),
                nb_inputs: 1,
                nb_outputs: 256,
                ..Default::default()
            },
        };

        assert_eq!(st.run_sample_network(0, 0, 0), 0);
    }

    #[test]
    fn test_run_frame_network_caps_frame_count_at_1000() {
        let mut st = LPCNetState::new();
        st.frame_count = 1000;
        let features = [0.0f32; NB_FEATURES];

        st.run_frame_network(&features);

        assert_eq!(st.frame_count, 1000);
    }

    #[test]
    fn test_run_sample_network_executes_non_empty_gru_path() {
        let mut st = LPCNetState::new();
        st.nnet.gru_a_state = vec![0.25, -0.25];
        st.nnet.gru_b_state = vec![0.10, -0.10];
        st.gru_a_condition = vec![0.01, -0.01, 0.02, -0.02, 0.03, -0.03];
        st.gru_b_condition = vec![0.04, -0.04, 0.05, -0.05, 0.06, -0.06];

        st.model.gru_a_embed_sig = EmbeddingLayer {
            weights: vec![0.10; 6],
            dim: 6,
        };
        st.model.gru_a_embed_pred = EmbeddingLayer {
            weights: vec![0.20; 6],
            dim: 6,
        };
        st.model.gru_a_embed_exc = EmbeddingLayer {
            weights: vec![-0.05; 6],
            dim: 6,
        };

        st.model.sparse_gru_a = LinearLayer {
            bias: Some(vec![0.1, -0.1, 0.2, -0.2, 0.3, -0.3]),
            nb_inputs: 2,
            nb_outputs: 6,
            ..Default::default()
        };
        st.model.gru_b_recurrent = LinearLayer {
            bias: Some(vec![0.05, -0.05, 0.15, -0.15, 0.25, -0.25]),
            nb_inputs: 2,
            nb_outputs: 6,
            ..Default::default()
        };
        st.model.gru_b_input_dense = LinearLayer {
            bias: Some(vec![0.30, -0.30]),
            nb_inputs: 2,
            nb_outputs: 2,
            ..Default::default()
        };
        st.model.dual_fc.fc1 = LinearLayer {
            nb_inputs: 2,
            nb_outputs: 256,
            ..Default::default()
        };
        st.model.dual_fc.fc2 = LinearLayer {
            nb_inputs: 2,
            nb_outputs: 256,
            ..Default::default()
        };
        st.sampling_logit_table.fill(-100.0);
        st.sampling_logit_table[13] = 0.0;

        let sampled = st.run_sample_network(0, 0, 0);
        assert_eq!(sampled, 13);
        assert!(st.nnet.gru_a_state.iter().all(|v| v.is_finite()));
        assert!(st.nnet.gru_b_state.iter().all(|v| v.is_finite()));
        assert_ne!(st.nnet.gru_a_state, vec![0.25, -0.25]);
        assert_ne!(st.nnet.gru_b_state, vec![0.10, -0.10]);
    }

    #[test]
    fn test_synthesize_and_blend_with_default_model() {
        let mut st = LPCNetState::new();
        let features = [0.0f32; NB_FEATURES];
        for _ in 0..=FEATURES_DELAY {
            st.run_frame_network(&features);
        }

        let mut synthesized = [0i16; FRAME_SIZE];
        st.synthesize(&features, &mut synthesized, FRAME_SIZE);
        assert!(
            synthesized
                .iter()
                .all(|&x| (-32767..=32767).contains(&(x as i32)))
        );

        let pcm_in = [123i16; FRAME_SIZE];
        let mut blended = [0i16; FRAME_SIZE];
        st.synthesize_blend(&pcm_in, &mut blended, FRAME_SIZE);
        assert_eq!(blended, pcm_in);
    }

    #[test]
    fn test_compute_single_frame_feature_wrappers_match_with_loaded_pitch_model() {
        let blob = make_pitchdnn_weight_blob();

        let mut enc_i16 = LPCNetEncState::new();
        let mut enc_f32 = LPCNetEncState::new();
        assert_eq!(enc_i16.load_model(&blob), 0);
        assert_eq!(enc_f32.load_model(&blob), 0);

        let pcm_i16 = patterned_pcm(17);
        let pcm_f32 = pcm_i16.map(|s| s as f32);
        let mut features_i16 = [0.0f32; NB_TOTAL_FEATURES];
        let mut features_f32 = [0.0f32; NB_TOTAL_FEATURES];

        assert_eq!(
            enc_i16.compute_single_frame_features(&pcm_i16, &mut features_i16),
            0
        );
        assert_eq!(
            enc_f32.compute_single_frame_features_float(&pcm_f32, &mut features_f32),
            0
        );
        assert!(features_i16.iter().all(|v| v.is_finite()));
        assert!(features_f32.iter().all(|v| v.is_finite()));
        for i in 0..NB_TOTAL_FEATURES {
            assert!((features_i16[i] - features_f32[i]).abs() < 1e-4);
        }
    }

    #[test]
    fn test_queue_features_rolls_buffer() {
        let mut plc = LPCNetPLCState::new();
        let first = [1.0f32; NB_FEATURES];
        let second = [2.0f32; NB_FEATURES];
        let third = [3.0f32; NB_FEATURES];

        plc.queue_features(&first);
        assert_eq!(
            &plc.cont_features[(CONT_VECTORS - 1) * NB_FEATURES..CONT_VECTORS * NB_FEATURES],
            &first
        );

        plc.queue_features(&second);
        assert_eq!(
            &plc.cont_features[(CONT_VECTORS - 2) * NB_FEATURES..(CONT_VECTORS - 1) * NB_FEATURES],
            &first
        );
        assert_eq!(
            &plc.cont_features[(CONT_VECTORS - 1) * NB_FEATURES..CONT_VECTORS * NB_FEATURES],
            &second
        );

        plc.queue_features(&third);
        assert_eq!(
            &plc.cont_features[(CONT_VECTORS - 3) * NB_FEATURES..(CONT_VECTORS - 2) * NB_FEATURES],
            &first
        );
        assert_eq!(
            &plc.cont_features[(CONT_VECTORS - 2) * NB_FEATURES..(CONT_VECTORS - 1) * NB_FEATURES],
            &second
        );
        assert_eq!(
            &plc.cont_features[(CONT_VECTORS - 1) * NB_FEATURES..CONT_VECTORS * NB_FEATURES],
            &third
        );
    }

    #[test]
    fn test_plc_state_bookkeeping() {
        let mut plc = LPCNetPLCState::new();
        let features = [0.0f32; NB_FEATURES];

        plc.fec_add(Some(&features));
        plc.fec_add(None);
        assert_eq!(plc.fec_fill_pos, 1);
        assert_eq!(plc.fec_skip, 1);
        plc.fec_clear();
        assert_eq!(plc.fec_fill_pos, 0);
        assert_eq!(plc.fec_skip, 0);
    }

    #[test]
    fn test_plc_update_resets_loss_and_blend() {
        let mut plc = LPCNetPLCState::new();
        let pcm = [0i16; FRAME_SIZE];

        plc.analysis_gap = false;
        plc.analysis_pos = FRAME_SIZE - 1;
        plc.predict_pos = FRAME_SIZE - 1;
        plc.loss_count = 7;
        plc.blend = 3;

        assert_eq!(plc.update(&pcm), 0);
        assert!(plc.analysis_gap);
        assert_eq!(plc.analysis_pos, FRAME_SIZE - 1);
        assert_eq!(plc.predict_pos, FRAME_SIZE - 1);
        assert_eq!(plc.loss_count, 0);
        assert_eq!(plc.blend, 0);
    }

    #[test]
    fn test_plc_get_fec_or_pred_prefers_fec_and_falls_back() {
        let mut plc = LPCNetPLCState::new();
        plc.loaded = true;
        plc.fec[0][0] = 0.75;
        plc.fec[0][NB_FEATURES - 1] = -0.25;
        plc.fec_fill_pos = 1;

        let mut out = [0.0f32; NB_FEATURES];
        assert!(plc.get_fec_or_pred(&mut out));
        assert_eq!(out, plc.fec[0]);
        assert_eq!(plc.fec_read_pos, 1);
        assert_eq!(plc.fec_skip, 0);

        let mut fallback = [0.0f32; NB_FEATURES];
        assert!(!plc.get_fec_or_pred(&mut fallback));
        assert_eq!(plc.fec_read_pos, 1);
        assert_eq!(plc.fec_skip, 0);

        plc.fec_skip = 2;
        let mut skipped = [0.0f32; NB_FEATURES];
        assert!(!plc.get_fec_or_pred(&mut skipped));
        assert_eq!(plc.fec_skip, 1);
        assert!(skipped.iter().all(|x| x.is_finite()));
    }

    #[test]
    fn test_plc_get_fec_or_pred_consumes_skip_with_pending_fec() {
        let mut plc = LPCNetPLCState::new();
        plc.loaded = true;
        plc.fec_fill_pos = 1;
        plc.fec[0][0] = 0.5;
        plc.fec_skip = 1;

        let mut out = [0.0f32; NB_FEATURES];
        assert!(!plc.get_fec_or_pred(&mut out));
        assert_eq!(plc.fec_read_pos, 0);
        assert_eq!(plc.fec_skip, 0);
        assert!(out.iter().all(|&v| v == 0.0));
    }

    #[test]
    fn test_plc_update_advances_history_when_positions_are_live() {
        let mut plc = LPCNetPLCState::new();
        let pcm = patterned_pcm(11);

        plc.analysis_gap = false;
        plc.analysis_pos = FRAME_SIZE;
        plc.predict_pos = FRAME_SIZE;
        plc.loss_count = 5;
        plc.blend = 3;

        assert_eq!(plc.update(&pcm), 0);
        assert!(!plc.analysis_gap);
        assert_eq!(plc.analysis_pos, 0);
        assert_eq!(plc.predict_pos, 0);
        assert_eq!(plc.loss_count, 0);
        assert_eq!(plc.blend, 0);
        assert_eq!(plc.pcm[PLC_BUF_SIZE - FRAME_SIZE], pcm[0] as f32 / 32768.0);
        assert_eq!(
            plc.pcm[PLC_BUF_SIZE - 1],
            pcm[FRAME_SIZE - 1] as f32 / 32768.0
        );
    }

    #[test]
    fn test_plc_conceal_runs_analysis_fec_and_attenuation_paths() {
        let blob = make_plc_weight_blob();
        let mut plc = LPCNetPLCState::new();
        assert_eq!(plc.load_model(&blob), 0);
        // Stage 7b.1 honest-gap: `load_model` parses but doesn't populate
        // model fields, so the public `loaded` flag stays `false`. To
        // exercise `conceal()` end-to-end (which `debug_assert!`s
        // `loaded`) we flip it manually. Post-7b.1.5 this manual flip
        // can be dropped — `load_model` will own the invariant.
        plc.loaded = true;

        for i in 0..PLC_BUF_SIZE {
            plc.pcm[i] = ((i % 29) as f32 - 14.0) / 32.0;
        }
        plc.analysis_gap = false;
        plc.analysis_pos = PLC_BUF_SIZE - FRAME_SIZE;
        plc.predict_pos = PLC_BUF_SIZE - FRAME_SIZE;
        plc.blend = 0;

        let mut first = [0i16; FRAME_SIZE];
        assert_eq!(plc.conceal(&mut first), 0);
        assert_eq!(plc.blend, 1);
        assert_eq!(plc.loss_count, 1);
        assert_eq!(plc.analysis_pos, PLC_BUF_SIZE - FRAME_SIZE);
        assert_eq!(plc.predict_pos, PLC_BUF_SIZE);
        assert!(
            first
                .iter()
                .all(|&s| (-32767..=32767).contains(&(s as i32)))
        );
        assert!(plc.features.iter().all(|v| v.is_finite()));
        assert!(plc.cont_features.iter().all(|v| v.is_finite()));

        let fec = [0.25f32; NB_FEATURES];
        plc.fec_add(Some(&fec));
        plc.loss_count = 4;
        let mut with_fec = [0i16; FRAME_SIZE];
        assert_eq!(plc.conceal(&mut with_fec), 0);
        assert_eq!(plc.loss_count, 0);

        plc.fec_clear();
        plc.loss_count = 9;
        let mut no_fec = [0i16; FRAME_SIZE];
        assert_eq!(plc.conceal(&mut no_fec), 0);
        assert_eq!(plc.loss_count, 10);
        assert!(plc.features[0] <= -3.0);
    }

    #[test]
    fn test_synthesize_tail_impl_preload_and_clip() {
        let mut st = LPCNetState::new();
        st.frame_count = FEATURES_DELAY as i32 + 1;
        st.lpc[0] = -2.0;

        let mut output = [20000i16, 123i16];
        st.synthesize_tail_impl(&mut output, 2, 1);
        assert_eq!(output[0], 20000);
        assert_eq!(output[1], 32767);
    }

    #[test]
    fn test_synthesize_tail_impl_negative_clip() {
        let mut st = LPCNetState::new();
        st.frame_count = FEATURES_DELAY as i32 + 1;
        st.last_sig[0] = 10_000.0;
        st.lpc[0] = 10.0;

        let mut output = [0i16; 1];
        st.synthesize_tail_impl(&mut output, 1, 0);

        assert_eq!(output[0], -32767);
    }

    #[test]
    fn test_synthesize_tail_zero_fills_during_initial_delay() {
        let mut st = LPCNetState::new();
        st.frame_count = FEATURES_DELAY as i32;
        let mut output = [99i16; FRAME_SIZE];
        st.synthesize_tail_impl(&mut output, FRAME_SIZE, 0);
        assert!(output.iter().all(|&x| x == 0));
    }

    #[test]
    fn test_complex_helpers_match_expected_arithmetic() {
        let a = Cpx { r: 2.0, i: 3.0 };
        let b = Cpx { r: -1.0, i: 4.0 };

        let mul = cmul(a, b);
        assert!((mul.r + 14.0).abs() < 1e-6);
        assert!((mul.i - 5.0).abs() < 1e-6);

        let mulc = cmulc(a, b);
        assert!((mulc.r - 10.0).abs() < 1e-6);
        assert!((mulc.i + 11.0).abs() < 1e-6);

        let add = cadd(a, b);
        assert!((add.r - 1.0).abs() < 1e-6);
        assert!((add.i - 7.0).abs() < 1e-6);

        let sub = csub(a, b);
        assert!((sub.r - 3.0).abs() < 1e-6);
        assert!((sub.i + 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_compute_band_energy_inverse_for_single_bin_signal() {
        let mut x = vec![Cpx::default(); FREQ_SIZE];
        x[0] = Cpx { r: 2.0, i: 0.0 };

        let mut inv = [0.0f32; NB_BANDS];
        compute_band_energy_inverse(&mut inv, &x);

        assert!(inv[0] > 0.0);
        assert!(inv.iter().all(|v| v.is_finite() && *v >= 0.0));
    }

    #[test]
    fn test_interp_band_gain_blends_between_neighboring_bands() {
        let mut gains = [99.0f32; FREQ_SIZE];
        let mut band_e = [0.0f32; NB_BANDS];
        band_e[0] = 1.0;
        band_e[1] = 3.0;
        band_e[2] = 6.0;

        interp_band_gain(&mut gains, &band_e);

        assert_eq!(gains[0], 1.0);
        assert!(gains[1] > gains[0]);
        assert!(gains[1] < 3.0);
        assert!(gains.iter().all(|v| v.is_finite() && *v >= 0.0));
    }

    #[test]
    fn test_lpc_from_cepstrum_and_weighting_keep_coefficients_finite() {
        let mut lpc = [0.0f32; LPC_ORDER];
        let mut cepstrum = [0.0f32; NB_BANDS];
        cepstrum[0] = 0.25;
        cepstrum[1] = -0.5;
        cepstrum[2] = 0.125;

        let err = lpc_from_cepstrum(&mut lpc, &cepstrum);
        assert!(err.is_finite());
        assert!(lpc.iter().all(|v| v.is_finite()));

        let weighted_before = lpc;
        lpc_weighting(&mut lpc, 0.9);
        assert!(lpc.iter().all(|v| v.is_finite()));
        assert!(
            lpc.iter()
                .zip(weighted_before.iter())
                .any(|(after, before)| after.abs() <= before.abs())
        );
    }

    #[test]
    fn test_energy_inner_product_and_pitch_helpers_cover_remainder_paths() {
        let x = [1.0f32, -2.0, 3.0, -4.0, 5.0];
        let y = [0.5f32, 1.0, -1.5, 2.0, -2.5, 3.0, -3.5];

        let energy = silk_energy_flp(&x, x.len());
        assert!((energy - 55.0).abs() < 1e-9);

        let inner = silk_inner_product_flp(&x, &y, x.len());
        assert!((inner + 26.5).abs() < 1e-9);

        let mut xcorr = [0.0f32; 3];
        celt_pitch_xcorr_f32(&x[..3], &y, &mut xcorr, 3, 3);
        assert!((xcorr[0] + 6.0).abs() < 1e-6);
        assert!((xcorr[1] - 10.0).abs() < 1e-6);
        assert!((xcorr[2] + 13.0).abs() < 1e-6);

        let history = [1.0f32, -1.0, 0.5, 2.0, 3.0];
        let num = [0.25f32, -0.5];
        let mut out = [0.0f32; 3];
        celt_fir_f32(&history, &num, &mut out, 3, 2);
        assert!((out[0] + 0.25).abs() < 1e-6);
        assert!((out[1] - 2.625).abs() < 1e-6);
        assert!((out[2] - 3.25).abs() < 1e-6);
    }

    #[test]
    fn test_preemphasis_and_biquad_update_state() {
        let input = [1.0f32, -0.5, 0.25];
        let mut pre = [0.0f32; 3];
        let mut mem = 0.5f32;
        preemphasis(&mut pre, &mut mem, &input, 0.85, input.len());
        assert!((pre[0] - 1.5).abs() < 1e-6);
        assert!((pre[1] + 1.35).abs() < 1e-6);
        assert!((pre[2] - 0.675).abs() < 1e-6);
        assert!((mem + 0.2125).abs() < 1e-6);

        let mut out = [0.0f32; 3];
        let mut biquad_mem = [0.0f32; 2];
        let b = [0.3f32, -0.2];
        let a = [0.1f32, 0.05];
        biquad(&mut out, &mut biquad_mem, &input, &b, &a, input.len());
        assert!(out.iter().all(|v| v.is_finite()));
        assert!(biquad_mem.iter().all(|v| v.is_finite()));
        assert!(biquad_mem.iter().any(|v| v.abs() > 0.0));
    }

    #[test]
    fn test_gru_input_and_mdense_cover_uncovered_paths() {
        let condition = [0.5f32, -0.5, 0.2, -0.2, 0.1, -0.1];
        let mut gru_a_input = [0.0f32; 6];
        let embed_sig = EmbeddingLayer {
            weights: vec![0.1; 6],
            dim: 6,
        };
        let embed_pred = EmbeddingLayer {
            weights: vec![0.4; 6],
            dim: 6,
        };
        let embed_exc = EmbeddingLayer {
            weights: vec![-0.05; 6],
            dim: 6,
        };
        compute_gru_a_input(
            &mut gru_a_input,
            &condition,
            2,
            &embed_sig,
            0,
            &embed_pred,
            1,
            &embed_exc,
            0,
        );
        for i in 0..6 {
            assert!((gru_a_input[i] - (condition[i] + 0.05)).abs() < 1e-6);
        }

        let recurrent = LinearLayer {
            bias: Some(vec![0.0, 0.1, -0.1, 0.2, 0.3, -0.3]),
            nb_inputs: 2,
            nb_outputs: 6,
            ..Default::default()
        };
        let mut sparse_state = vec![0.2f32, -0.1];
        let sparse_input = [0.3f32, -0.3, 0.2, -0.2, 0.0, 0.0];
        compute_sparse_gru(&recurrent, &mut sparse_state, &sparse_input);
        assert!(sparse_state.iter().all(|v| v.is_finite()));
        assert_ne!(sparse_state, vec![0.2, -0.1]);

        let input_dense = LinearLayer {
            bias: Some(vec![0.25, -0.25]),
            nb_inputs: 2,
            nb_outputs: 2,
            ..Default::default()
        };
        let mut gru_input = vec![0.1f32, -0.1, 0.2, -0.2, 0.0, 0.0];
        let mut gru_b_state = vec![0.4f32, -0.4];
        let extra = [0.5f32, -0.5];
        compute_gru_b(
            &recurrent,
            &input_dense,
            &mut gru_input,
            &mut gru_b_state,
            &extra,
        );
        assert!(gru_b_state.iter().all(|v| v.is_finite()));
        assert_ne!(gru_b_state, vec![0.4, -0.4]);

        let mdense = MDenseLayer {
            fc1: LinearLayer {
                nb_inputs: 2,
                nb_outputs: 256,
                ..Default::default()
            },
            fc2: LinearLayer {
                nb_inputs: 2,
                nb_outputs: 256,
                ..Default::default()
            },
        };
        let state = [0.0f32, 0.0];
        let mut logit_table = [-100.0f32; 256];
        logit_table[7] = 0.0;
        let mut rng = Kiss99Ctx::default();
        assert_eq!(sample_mdense(&mdense, &state, &logit_table, &mut rng), 7);

        let zero_sum_logits = [-100.0f32; 256];
        let mut rng2 = Kiss99Ctx::default();
        assert_eq!(
            sample_mdense(&mdense, &state, &zero_sum_logits, &mut rng2),
            255
        );
    }

    // --- Coverage additions: PLC, encoder features, LPC analysis ---

    #[test]
    fn test_preemphasis_basic() {
        let x = [0.5f32, 0.3, 0.1, 0.0, -0.1];
        let mut y = [0.0f32; 5];
        let mut mem = 0.0f32;
        preemphasis(&mut y, &mut mem, &x, 0.85, 5);
        // First output: y[0] = x[0] + mem = 0.5 + 0.0 = 0.5
        assert!(
            (y[0] - 0.5).abs() < 1e-5,
            "first preemph output should be 0.5, got {}",
            y[0]
        );
        // mem should be -coef * x[last] = -0.85 * (-0.1) = 0.085
        assert!(
            (mem - 0.085).abs() < 1e-5,
            "mem should be -coef * x[last], got {}",
            mem
        );
    }

    #[test]
    fn test_lpc_from_cepstrum_basic() {
        let mut lpc = [0.0f32; LPC_ORDER];
        let cepstrum = [0.0f32; NB_BANDS]; // zero cepstrum -> flat spectrum
        let _gain = lpc_from_cepstrum(&mut lpc, &cepstrum);
        // With zero cepstrum, LPC should be near zero (white spectrum)
        assert!(
            lpc.iter().all(|&c| c.is_finite()),
            "LPC coefficients should be finite"
        );
    }

    #[test]
    fn test_dct_basic() {
        let input = [1.0f32; NB_BANDS]; // DC input
        let mut output = [0.0f32; NB_BANDS];
        dct(&mut output, &input);
        // DC component should be non-zero
        assert!(
            output[0].abs() > 0.0,
            "DCT of DC signal should have non-zero DC component"
        );
        assert!(output[0].is_finite());
    }

    #[test]
    fn test_apply_window_basic() {
        let mut x = [1.0f32; WINDOW_SIZE];
        apply_window(&mut x);
        // Windowed signal: edges should be attenuated, middle should be near 1.0
        assert!(x[0].abs() < 1.0, "window should attenuate edges");
        let mid = WINDOW_SIZE / 2;
        assert!(
            x[mid].abs() > 0.5,
            "window should preserve middle: got {}",
            x[mid]
        );
    }

    #[test]
    fn test_lpc_weighting_reduces_coefficients() {
        let mut lpc = [1.0f32; LPC_ORDER];
        lpc_weighting(&mut lpc, 0.9);
        // Each coefficient should be scaled by gamma^(i+1)
        assert!(
            (lpc[0] - 0.9).abs() < 1e-5,
            "lpc[0] should be 0.9, got {}",
            lpc[0]
        );
        assert!(
            lpc[LPC_ORDER - 1].abs() < lpc[0].abs(),
            "later coefficients should be smaller"
        );
    }

    #[test]
    fn test_encoder_state_new_and_compute_features() {
        let mut enc = LPCNetEncState::new();
        // Compute features on a patterned frame
        let input = patterned_frame(42);
        enc.compute_frame_features(&input);
        // Features should be finite
        assert!(
            enc.features[0..NB_BANDS].iter().all(|f| f.is_finite()),
            "encoder features should be finite after compute"
        );
    }

    #[test]
    fn test_kiss99_determinism() {
        let mut rng1 = Kiss99Ctx::default();
        let mut rng2 = Kiss99Ctx::default();
        let v1: Vec<u32> = (0..10).map(|_| rng1.rand()).collect();
        let v2: Vec<u32> = (0..10).map(|_| rng2.rand()).collect();
        assert_eq!(v1, v2, "same seed should produce same sequence");
    }

    // ------------------------------------------------------------------
    // Stage 6 branch-coverage additions
    // ------------------------------------------------------------------

    mod branch_coverage_stage6 {
        use super::*;

        // L613 true branch: fmin where a < b (rarely reached via internal callers).
        #[test]
        fn test_fmin_a_less_than_b_returns_a() {
            assert_eq!(fmin(-1.0, 2.0), -1.0);
            assert_eq!(fmin(0.5, 1.0), 0.5);
        }

        // L1076 false branch: EmbeddingLayer::compute with idx out of range
        // falls through to the zero-fill path.
        #[test]
        fn test_embedding_compute_out_of_range_fills_zero() {
            let emb = EmbeddingLayer {
                weights: vec![1.0, 2.0, 3.0, 4.0],
                dim: 2,
            };
            let mut out = [9.0f32, 9.0];
            emb.compute(&mut out, 5); // start=10 > weights.len()=4
            assert_eq!(out, [0.0, 0.0]);
        }

        #[test]
        fn test_embedding_accum_out_of_range_is_noop() {
            let emb = EmbeddingLayer {
                weights: vec![1.0, 2.0],
                dim: 2,
            };
            let mut out = [7.0f32, 7.0];
            emb.accum(&mut out, 10);
            assert_eq!(out, [7.0, 7.0]);
        }

        // L2195 false branch: conceal() with blend != 0 and analysis_pos < FRAME_SIZE
        // sets analysis_gap = true.
        #[test]
        fn test_plc_conceal_blend_active_sets_analysis_gap() {
            let blob = make_plc_weight_blob();
            let mut plc = LPCNetPLCState::new();
            assert_eq!(plc.load_model(&blob), 0);
            // Stage 7b.1 honest-gap: flip `loaded` manually to unlock
            // `conceal()`'s debug_assert; 7b.1.5 will make `load_model`
            // own this invariant once model fields are populated.
            plc.loaded = true;
            plc.blend = 1; // skip while-loop body inside conceal
            plc.analysis_pos = 0; // so analysis_pos < FRAME_SIZE at the tail check
            plc.analysis_gap = false;
            plc.predict_pos = FRAME_SIZE;
            plc.loss_count = 0;
            // When blend != 0 we skip the fargan.cont() init inside conceal, so prime
            // the FARGAN continuation here to satisfy the debug_assert in synthesize_int.
            let cont_pcm = vec![0.0f32; FARGAN_CONT_SAMPLES];
            let cont_feat = vec![0.0f32; CONT_VECTORS * NB_FEATURES];
            plc.fargan.cont(&cont_pcm, &cont_feat);

            let mut pcm = [0i16; FRAME_SIZE];
            assert_eq!(plc.conceal(&mut pcm), 0);
            assert!(plc.analysis_gap);
        }

        // L2127 partial coverage: conceal() with analysis_gap=true forces
        // the false side of (!analysis_gap || count > 0) at count==0, and then
        // the true side of count > 0 on subsequent iterations.
        #[test]
        fn test_plc_conceal_with_analysis_gap_true_short_circuits_feature_update() {
            let blob = make_plc_weight_blob();
            let mut plc = LPCNetPLCState::new();
            assert_eq!(plc.load_model(&blob), 0);
            // Stage 7b.1 honest-gap — see sibling test.
            plc.loaded = true;
            for i in 0..PLC_BUF_SIZE {
                plc.pcm[i] = ((i % 17) as f32 - 8.0) / 64.0;
            }
            plc.analysis_gap = true;
            // Two while-loop iterations: count goes 0 -> 1, so second iter hits
            // the count > 0 true branch of the compound condition.
            plc.analysis_pos = PLC_BUF_SIZE - 2 * FRAME_SIZE;
            plc.predict_pos = PLC_BUF_SIZE - 2 * FRAME_SIZE;
            plc.blend = 0;
            plc.loss_count = 0;

            let mut pcm = [0i16; FRAME_SIZE];
            assert_eq!(plc.conceal(&mut pcm), 0);
        }

        // Exercise the opposite side of the predict-pos compare: analysis_pos < predict_pos
        // hits the false branch of self.analysis_pos >= self.predict_pos.
        #[test]
        fn test_plc_conceal_analysis_behind_predict_skips_feature_update() {
            let blob = make_plc_weight_blob();
            let mut plc = LPCNetPLCState::new();
            assert_eq!(plc.load_model(&blob), 0);
            // Stage 7b.1 honest-gap — see sibling test.
            plc.loaded = true;
            for i in 0..PLC_BUF_SIZE {
                plc.pcm[i] = ((i % 13) as f32 - 6.0) / 64.0;
            }
            plc.analysis_gap = false;
            plc.analysis_pos = PLC_BUF_SIZE - FRAME_SIZE;
            plc.predict_pos = PLC_BUF_SIZE; // > analysis_pos
            plc.blend = 0;
            plc.loss_count = 0;

            let mut pcm = [0i16; FRAME_SIZE];
            assert_eq!(plc.conceal(&mut pcm), 0);
        }

        // Kiss99: cover the three short-cycle fix branches in srand (L96, L99, L102).
        // We cannot easily drive srand to produce z==0 or the magic constants, but we
        // can independently drive rand() from a manually-crafted state that starts at
        // those boundary values -- this won't hit srand's branches but does exercise
        // rand()'s state machine near fixed points.
        #[test]
        fn test_kiss99_near_boundary_states_still_deterministic() {
            let mut a = Kiss99Ctx {
                z: 1,
                w: 1,
                jsr: 1,
                jcong: 0,
            };
            let mut b = a.clone();
            for _ in 0..16 {
                assert_eq!(a.rand(), b.rand());
            }
        }

        // Try a variety of pathological signals in silk_burg_analysis: one of them
        // may drive `nrg_f - FIND_LPC_COND_FAC * c0_full * t1` negative, covering the
        // true side of the final `nrg_f < 0.0` clamp.
        #[test]
        fn test_silk_burg_analysis_on_adversarial_signals() {
            let configs: &[(&[f32], usize, usize)] = &[
                (&[0.0f32; 48][..], 48, 1),
                (&[1.0f32; 48][..], 48, 1),
                (&[-1.0f32; 48][..], 48, 1),
                (&[1e-20f32; 48][..], 48, 1),
            ];
            for (data, len, nb) in configs {
                let mut a = [0.0f32; LPC_ORDER];
                let v = silk_burg_analysis(&mut a, data, 1e-3, *len, *nb, LPC_ORDER);
                assert!(v.is_finite());
            }

            // Alternating impulse: introduces sharp correlation that can stress
            // the numerical stabilization inside the Burg recursion.
            let mut alt = [0.0f32; 96];
            for (i, v) in alt.iter_mut().enumerate() {
                *v = if i % 2 == 0 { 1.0 } else { -1.0 };
            }
            let mut a = [0.0f32; LPC_ORDER];
            let v = silk_burg_analysis(&mut a, &alt, 1e-3, 96, 1, LPC_ORDER);
            assert!(v.is_finite());

            // Ramp with saturation-like behavior.
            let mut ramp = [0.0f32; 96];
            for (i, v) in ramp.iter_mut().enumerate() {
                *v = (i as f32 / 95.0) * 2.0 - 1.0;
            }
            let mut a2 = [0.0f32; LPC_ORDER];
            let v2 = silk_burg_analysis(&mut a2, &ramp, 1e-3, 96, 1, LPC_ORDER);
            assert!(v2.is_finite());

            // Very small min_inv_gain -> forces reached_max path rarely.
            let mut a3 = [0.0f32; LPC_ORDER];
            let v3 = silk_burg_analysis(&mut a3, &alt, 1e-30, 96, 1, LPC_ORDER);
            assert!(v3.is_finite());

            // Large min_inv_gain (>=1) forces reached_max=true immediately: the
            // resulting c0 subtraction can go negative for correlated signals,
            // covering the nrg_f < 0.0 branch.
            let mut a4 = [0.0f32; LPC_ORDER];
            let v4 = silk_burg_analysis(&mut a4, &alt, 10.0, 96, 1, LPC_ORDER);
            assert!(v4.is_finite());

            // Same with zeros input - c0_full is 1e-9 but large min_inv_gain clamps
            // things immediately.
            let mut a5 = [0.0f32; LPC_ORDER];
            let v5 = silk_burg_analysis(&mut a5, &[0.0f32; 48], 10.0, 48, 1, LPC_ORDER);
            assert!(v5.is_finite());

            // Pure DC signal stresses the correlation math.
            let mut a6 = [0.0f32; LPC_ORDER];
            let dc = [0.99f32; 96];
            let v6 = silk_burg_analysis(&mut a6, &dc, 100.0, 96, 1, LPC_ORDER);
            assert!(v6.is_finite());
        }
    }
}
