Skip to main content

ftts_kernels/
enhance.rs

1//! FastEnhancer-S 48 kHz speech denoiser — a pure-Rust port of the pinned reference.
2//!
3//! Reference: `aask1357/fastenhancer` @ `f85223bd546b27f39dc0744e0310dcd246f750a4`,
4//! checkpoint release `ckpt-v1.0.0-48khz` / `fastenhancer_s.zip` (MIT). The port consumes
5//! the *inference-form* weights: the reference's own `remove_weight_reparameterizations()`
6//! folds every weight-norm and BatchNorm into plain conv/linear weight+bias before export,
7//! so this engine implements only convolutions, GRUs, one tiny frequency attention, and
8//! the compressed-STFT front/back ends.
9//!
10//! Geometry (the `s` config, asserted at load): n_fft 1024, hop 512, 64 encoder channels,
11//! stride-4 frequency downsample (512 -> 128 bins), 3 RNNFormer blocks at 48 channels x 48
12//! frequency slots with 4 attention heads, complex ratio mask output.
13//!
14//! Everything is time-causal except the STFT overlap-add; the whole model runs per frame
15//! with GRU state carried across frames, so the offline and streaming decompositions are
16//! the same arithmetic.
17
18use std::collections::BTreeMap;
19use std::fmt;
20
21pub const SAMPLE_RATE_HZ: u32 = 48_000;
22
23const N_FFT: usize = 1024;
24const HOP: usize = 512;
25/// Model bins: the reference discards the Nyquist bin (`discard_last_freq_bin`).
26const FREQ: usize = N_FFT / 2;
27const CH: usize = 64;
28const STRIDE: usize = 4;
29const K0: usize = 8;
30const F_ENC: usize = FREQ / STRIDE;
31const ENC_CONVS: usize = 3;
32const ENC_K: usize = 3;
33const RF_CH: usize = 48;
34const RF_FREQ: usize = 48;
35const HEADS: usize = 4;
36const HEAD_DIM: usize = RF_CH / HEADS;
37const BLOCKS: usize = 3;
38const COMPRESSION: f32 = 0.3;
39const MAG_EPS: f32 = 1.0e-5;
40
41#[derive(Debug)]
42pub enum EnhanceError {
43    MissingTensor(String),
44    ShapeMismatch {
45        name: String,
46        expected: Vec<usize>,
47        got: Vec<usize>,
48    },
49}
50
51impl fmt::Display for EnhanceError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::MissingTensor(name) => write!(f, "enhancer tensor {name} is missing"),
55            Self::ShapeMismatch {
56                name,
57                expected,
58                got,
59            } => {
60                write!(
61                    f,
62                    "enhancer tensor {name}: expected shape {expected:?}, got {got:?}"
63                )
64            }
65        }
66    }
67}
68
69impl std::error::Error for EnhanceError {}
70
71struct Conv1d {
72    /// `[out][in][k]` flattened.
73    weight: Vec<f32>,
74    bias: Vec<f32>,
75    out_ch: usize,
76    in_ch: usize,
77    k: usize,
78}
79
80struct Linear {
81    /// `[out][in]` flattened.
82    weight: Vec<f32>,
83}
84
85struct GruWeights {
86    /// `[3*H][H]` flattened, gate order r, z, n (PyTorch layout).
87    weight_ih: Vec<f32>,
88    weight_hh: Vec<f32>,
89    bias_ih: Vec<f32>,
90    bias_hh: Vec<f32>,
91}
92
93struct RnnFormerBlock {
94    rnn: GruWeights,
95    rnn_fc: Conv1d,
96    qkv: Linear,
97    attn_fc: Conv1d,
98    /// `[RF_FREQ][RF_CH]`, block 0 only.
99    pe: Option<Vec<f32>>,
100}
101
102/// The complete inference-form parameter set.
103pub struct Enhancer {
104    /// enc_pre remapped to a direct strided conv: `[CH][2][K0]`,
105    /// kernel index `kk*STRIDE + si` (see `load` for the derivation).
106    enc_pre: Conv1d,
107    encoder: Vec<Conv1d>,
108    rf_pre_lin: Linear,
109    rf_pre_conv: Conv1d,
110    blocks: Vec<RnnFormerBlock>,
111    rf_post_lin: Linear,
112    rf_post_conv: Conv1d,
113    /// Per decoder stage: 1x1 concat-mix conv then k=3 conv.
114    decoder: Vec<(Conv1d, Conv1d)>,
115    dec_post_conv: Conv1d,
116    /// ConvTranspose1d `[in=CH][out=2][K0]` flattened, plus bias `[2]`.
117    dec_post_up: Vec<f32>,
118    dec_post_up_bias: Vec<f32>,
119    window: Vec<f32>,
120    fft: Fft,
121}
122
123/// One tensor as handed to [`Enhancer::load`]: shape and row-major data.
124pub type TensorEntry = (Vec<usize>, Vec<f32>);
125
126fn take(
127    tensors: &mut BTreeMap<String, TensorEntry>,
128    name: &str,
129    expected: &[usize],
130) -> Result<Vec<f32>, EnhanceError> {
131    let (shape, data) = tensors
132        .remove(name)
133        .ok_or_else(|| EnhanceError::MissingTensor(name.to_owned()))?;
134    if shape != expected {
135        return Err(EnhanceError::ShapeMismatch {
136            name: name.to_owned(),
137            expected: expected.to_vec(),
138            got: shape,
139        });
140    }
141    Ok(data)
142}
143
144fn silu(x: f32) -> f32 {
145    x / (1.0 + (-x).exp())
146}
147
148fn sigmoid(x: f32) -> f32 {
149    1.0 / (1.0 + (-x).exp())
150}
151
152impl Enhancer {
153    /// Builds the engine from named inference-form tensors (reference `named_parameters()`
154    /// plus the `buffer.stft.window` buffer). Consumes the map; leftover tensors are ignored
155    /// so callers may pass a superset artifact.
156    pub fn load(mut tensors: BTreeMap<String, TensorEntry>) -> Result<Self, EnhanceError> {
157        // enc_pre.0 is the reference's StridedConv1d: Conv1d(2*S, CH, K0/S) over the
158        // stride-S-reshaped input. With reshaped channel index `si*2 + c` and kernel
159        // position `kk`, output j reads x_pad[c][(j+kk)*S + si] = x_pad[c][j*S + (kk*S+si)],
160        // i.e. a plain stride-S conv whose kernel index is m = kk*S + si.
161        let raw = take(
162            &mut tensors,
163            "enc_pre.0.weight",
164            &[CH, 2 * STRIDE, K0 / STRIDE],
165        )?;
166        let bias = take(&mut tensors, "enc_pre.0.bias", &[CH])?;
167        let mut w = vec![0.0f32; CH * 2 * K0];
168        for o in 0..CH {
169            for si in 0..STRIDE {
170                for c in 0..2 {
171                    for kk in 0..K0 / STRIDE {
172                        let m = kk * STRIDE + si;
173                        w[(o * 2 + c) * K0 + m] =
174                            raw[(o * (2 * STRIDE) + si * 2 + c) * (K0 / STRIDE) + kk];
175                    }
176                }
177            }
178        }
179        let enc_pre = Conv1d {
180            weight: w,
181            bias,
182            out_ch: CH,
183            in_ch: 2,
184            k: K0,
185        };
186
187        let mut encoder = Vec::with_capacity(ENC_CONVS);
188        for i in 0..ENC_CONVS {
189            encoder.push(Conv1d {
190                weight: take(
191                    &mut tensors,
192                    &format!("encoder.{i}.0.weight"),
193                    &[CH, CH, ENC_K],
194                )?,
195                bias: take(&mut tensors, &format!("encoder.{i}.0.bias"), &[CH])?,
196                out_ch: CH,
197                in_ch: CH,
198                k: ENC_K,
199            });
200        }
201
202        let rf_pre_lin = Linear {
203            weight: take(&mut tensors, "rf_pre.0.weight", &[RF_FREQ, F_ENC])?,
204        };
205        let rf_pre_conv = Conv1d {
206            weight: take(&mut tensors, "rf_pre.1.weight", &[RF_CH, CH, 1])?,
207            bias: take(&mut tensors, "rf_pre.1.bias", &[RF_CH])?,
208            out_ch: RF_CH,
209            in_ch: CH,
210            k: 1,
211        };
212
213        let mut blocks = Vec::with_capacity(BLOCKS);
214        for i in 0..BLOCKS {
215            let pe = if i == 0 {
216                Some(take(&mut tensors, "rf_block.0.pe", &[RF_FREQ, RF_CH])?)
217            } else {
218                None
219            };
220            blocks.push(RnnFormerBlock {
221                rnn: GruWeights {
222                    weight_ih: take(
223                        &mut tensors,
224                        &format!("rf_block.{i}.rnn.weight_ih_l0"),
225                        &[3 * RF_CH, RF_CH],
226                    )?,
227                    weight_hh: take(
228                        &mut tensors,
229                        &format!("rf_block.{i}.rnn.weight_hh_l0"),
230                        &[3 * RF_CH, RF_CH],
231                    )?,
232                    bias_ih: take(
233                        &mut tensors,
234                        &format!("rf_block.{i}.rnn.bias_ih_l0"),
235                        &[3 * RF_CH],
236                    )?,
237                    bias_hh: take(
238                        &mut tensors,
239                        &format!("rf_block.{i}.rnn.bias_hh_l0"),
240                        &[3 * RF_CH],
241                    )?,
242                },
243                rnn_fc: Conv1d {
244                    weight: take(
245                        &mut tensors,
246                        &format!("rf_block.{i}.rnn_fc.weight"),
247                        &[RF_CH, RF_CH],
248                    )?,
249                    bias: take(&mut tensors, &format!("rf_block.{i}.rnn_fc.bias"), &[RF_CH])?,
250                    out_ch: RF_CH,
251                    in_ch: RF_CH,
252                    k: 1,
253                },
254                qkv: Linear {
255                    weight: take(
256                        &mut tensors,
257                        &format!("rf_block.{i}.attn.qkv.weight"),
258                        &[3 * RF_CH, RF_CH],
259                    )?,
260                },
261                attn_fc: Conv1d {
262                    weight: take(
263                        &mut tensors,
264                        &format!("rf_block.{i}.attn_fc.weight"),
265                        &[RF_CH, RF_CH],
266                    )?,
267                    bias: take(
268                        &mut tensors,
269                        &format!("rf_block.{i}.attn_fc.bias"),
270                        &[RF_CH],
271                    )?,
272                    out_ch: RF_CH,
273                    in_ch: RF_CH,
274                    k: 1,
275                },
276                pe,
277            });
278        }
279
280        let rf_post_lin = Linear {
281            weight: take(&mut tensors, "rf_post.0.weight", &[F_ENC, RF_FREQ])?,
282        };
283        let rf_post_conv = Conv1d {
284            weight: take(&mut tensors, "rf_post.1.weight", &[CH, RF_CH, 1])?,
285            bias: take(&mut tensors, "rf_post.1.bias", &[CH])?,
286            out_ch: CH,
287            in_ch: RF_CH,
288            k: 1,
289        };
290
291        let mut decoder = Vec::with_capacity(ENC_CONVS);
292        for i in 0..ENC_CONVS {
293            decoder.push((
294                Conv1d {
295                    weight: take(
296                        &mut tensors,
297                        &format!("decoder.{i}.0.weight"),
298                        &[CH, 2 * CH, 1],
299                    )?,
300                    bias: take(&mut tensors, &format!("decoder.{i}.0.bias"), &[CH])?,
301                    out_ch: CH,
302                    in_ch: 2 * CH,
303                    k: 1,
304                },
305                Conv1d {
306                    weight: take(
307                        &mut tensors,
308                        &format!("decoder.{i}.2.weight"),
309                        &[CH, CH, ENC_K],
310                    )?,
311                    bias: take(&mut tensors, &format!("decoder.{i}.2.bias"), &[CH])?,
312                    out_ch: CH,
313                    in_ch: CH,
314                    k: ENC_K,
315                },
316            ));
317        }
318
319        let dec_post_conv = Conv1d {
320            weight: take(&mut tensors, "dec_post.0.weight", &[CH, 2 * CH, 1])?,
321            bias: take(&mut tensors, "dec_post.0.bias", &[CH])?,
322            out_ch: CH,
323            in_ch: 2 * CH,
324            k: 1,
325        };
326        let dec_post_up = take(&mut tensors, "dec_post.2.weight", &[CH, 2, K0])?;
327        let dec_post_up_bias = take(&mut tensors, "dec_post.2.bias", &[2])?;
328        let window = take(&mut tensors, "buffer.stft.window", &[N_FFT])?;
329
330        Ok(Self {
331            enc_pre,
332            encoder,
333            rf_pre_lin,
334            rf_pre_conv,
335            blocks,
336            rf_post_lin,
337            rf_post_conv,
338            decoder,
339            dec_post_conv,
340            dec_post_up,
341            dec_post_up_bias,
342            window,
343            fft: Fft::new(N_FFT),
344        })
345    }
346
347    /// Denoises a 48 kHz mono clip. Returns `(frames - 1) * hop` samples where
348    /// `frames = wav.len() / hop + 1` (the reference's centered-STFT round trip);
349    /// pad the input to a hop multiple to keep the full length.
350    pub fn enhance_48k(&self, wav: &[f32]) -> Vec<f32> {
351        // Below one hop the length contract is zero samples anyway ((frames-1)*hop == 0),
352        // and the reflect-padding walk below does not terminate for 0- or 1-sample input —
353        // reflection needs more signal than padding. Return the contracted empty answer.
354        if wav.len() < HOP {
355            return Vec::new();
356        }
357        let frames = wav.len() / HOP + 1;
358        let mut state = self.new_state();
359        // Compressed spectrum per frame, then masked spectrum accumulated into OLA.
360        let mut out = vec![0.0f32; (frames - 1) * HOP + N_FFT];
361        let mut winsq = vec![0.0f32; (frames - 1) * HOP + N_FFT];
362        let mut spec = [0.0f32; 2 * (FREQ + 1)];
363        let mut scratch_time = vec![0.0f32; N_FFT];
364
365        for t in 0..frames {
366            self.frame_spectrum(wav, t, &mut spec, &mut scratch_time);
367            // Compress: x * max(|x|, eps)^(c-1), Nyquist bin discarded.
368            let mut comp = [0.0f32; 2 * FREQ];
369            for f in 0..FREQ {
370                let re = spec[2 * f];
371                let im = spec[2 * f + 1];
372                let mag = (re * re + im * im).sqrt().max(MAG_EPS);
373                let g = mag.powf(COMPRESSION - 1.0);
374                comp[2 * f] = re * g;
375                comp[2 * f + 1] = im * g;
376            }
377            let mask = self.frame_forward(&comp, &mut state);
378            // spec_hat = comp * mask (complex), then uncompress by |spec_hat|^(1/c - 1).
379            let mut frame_spec = [0.0f32; 2 * (FREQ + 1)];
380            for f in 0..FREQ {
381                let (ar, ai) = (comp[2 * f], comp[2 * f + 1]);
382                let (br, bi) = (mask[2 * f], mask[2 * f + 1]);
383                let re = ar * br - ai * bi;
384                let im = ar * bi + ai * br;
385                let mag = (re * re + im * im).sqrt();
386                let g = if mag > 0.0 {
387                    mag.powf(1.0 / COMPRESSION - 1.0)
388                } else {
389                    0.0
390                };
391                frame_spec[2 * f] = re * g;
392                frame_spec[2 * f + 1] = im * g;
393            }
394            self.fft.irfft(&frame_spec, &mut scratch_time);
395            let base = t * HOP;
396            for i in 0..N_FFT {
397                out[base + i] += scratch_time[i] * self.window[i];
398                winsq[base + i] += self.window[i] * self.window[i];
399            }
400        }
401
402        // torch.istft: normalize by the window-square envelope, trim n_fft/2 padding.
403        let start = N_FFT / 2;
404        let len = (frames - 1) * HOP;
405        let mut result = Vec::with_capacity(len);
406        for i in 0..len {
407            let w = winsq[start + i];
408            result.push(if w > 1.0e-11 { out[start + i] / w } else { 0.0 });
409        }
410        result
411    }
412
413    /// Denoises a 24 kHz mono clip: up to the model's native 48 kHz, through the network,
414    /// back down to 24 kHz, preserving length exactly.
415    ///
416    /// This is the shape both product surfaces consume (the engine's pipeline is 24 kHz
417    /// end to end); `enhance_48k` stays public for callers already at the native rate.
418    pub fn enhance_24k(&self, wav24k: &[f32]) -> Vec<f32> {
419        let mut wav48 = resample_lanczos6(wav24k, 24_000, SAMPLE_RATE_HZ);
420        let target_len = wav48.len();
421        // On the hop grid the STFT round trip returns every sample (see enhance_48k).
422        let padded = target_len.div_ceil(HOP) * HOP;
423        wav48.resize(padded, 0.0);
424        let mut enhanced = self.enhance_48k(&wav48);
425        enhanced.truncate(target_len);
426        let mut back = resample_lanczos6(&enhanced, SAMPLE_RATE_HZ, 24_000);
427        back.truncate(wav24k.len());
428        back
429    }
430
431    fn new_state(&self) -> Vec<Vec<f32>> {
432        vec![vec![0.0f32; RF_FREQ * RF_CH]; BLOCKS]
433    }
434
435    /// Centered, reflect-padded, windowed rFFT of frame `t`.
436    fn frame_spectrum(&self, wav: &[f32], t: usize, spec: &mut [f32], time: &mut [f32]) {
437        let n = wav.len() as isize;
438        let start = t as isize * HOP as isize - (N_FFT / 2) as isize;
439        for (i, slot) in time.iter_mut().enumerate() {
440            let mut idx = start + i as isize;
441            // torch reflect padding (no edge repetition). The clip is longer than one
442            // reflection order for any real enrollment input; iterate for tiny inputs.
443            loop {
444                if idx < 0 {
445                    idx = -idx;
446                } else if idx >= n {
447                    idx = 2 * (n - 1) - idx;
448                } else {
449                    break;
450                }
451            }
452            *slot = wav[idx as usize] * self.window[i];
453        }
454        self.fft.rfft(time, spec);
455    }
456
457    /// One frame through encoder / RNNFormer / decoder; returns the complex mask.
458    fn frame_forward(&self, comp: &[f32], state: &mut [Vec<f32>]) -> [f32; 2 * FREQ] {
459        // ---- encoder prenet: [2][FREQ] -> [CH][F_ENC], direct stride-4 conv -----------
460        // Input layout for the conv: channel 0 = real, channel 1 = imag.
461        let pad = (K0 - STRIDE) / 2;
462        let mut x = vec![0.0f32; CH * F_ENC];
463        for o in 0..CH {
464            let w = &self.enc_pre.weight[o * 2 * K0..(o + 1) * 2 * K0];
465            let b = self.enc_pre.bias[o];
466            for j in 0..F_ENC {
467                let mut acc = b;
468                for m in 0..K0 {
469                    let f = (j * STRIDE + m) as isize - pad as isize;
470                    if f >= 0 && (f as usize) < FREQ {
471                        let f = f as usize;
472                        acc += w[m] * comp[2 * f] + w[K0 + m] * comp[2 * f + 1];
473                    }
474                }
475                x[o * F_ENC + j] = silu(acc);
476            }
477        }
478
479        // ---- encoder stack, keeping skip outputs ---------------------------------------
480        let mut skips: Vec<Vec<f32>> = Vec::with_capacity(1 + ENC_CONVS);
481        skips.push(x.clone());
482        for conv in &self.encoder {
483            x = conv_k_same(conv, &x, F_ENC, true);
484            skips.push(x.clone());
485        }
486
487        // ---- RNNFormer prenet: freq linear then 1x1 channel mix ------------------------
488        // x: [CH][F_ENC] -> lin over freq -> [CH][RF_FREQ] -> conv1x1 -> [RF_CH][RF_FREQ]
489        let mut xf = vec![0.0f32; CH * RF_FREQ];
490        for c in 0..CH {
491            let row = &x[c * F_ENC..(c + 1) * F_ENC];
492            for (fr, slot) in xf[c * RF_FREQ..(c + 1) * RF_FREQ].iter_mut().enumerate() {
493                let w = &self.rf_pre_lin.weight[fr * F_ENC..(fr + 1) * F_ENC];
494                let mut acc = 0.0f32;
495                for f in 0..F_ENC {
496                    acc += w[f] * row[f];
497                }
498                *slot = acc;
499            }
500        }
501        // Transpose into token-major [RF_FREQ][RF_CH] while mixing channels.
502        let mut tokens = vec![0.0f32; RF_FREQ * RF_CH];
503        for oc in 0..RF_CH {
504            let w = &self.rf_pre_conv.weight[oc * CH..(oc + 1) * CH];
505            let b = self.rf_pre_conv.bias[oc];
506            for fr in 0..RF_FREQ {
507                let mut acc = b;
508                for ic in 0..CH {
509                    acc += w[ic] * xf[ic * RF_FREQ + fr];
510                }
511                tokens[fr * RF_CH + oc] = acc;
512            }
513        }
514
515        // ---- RNNFormer blocks -----------------------------------------------------------
516        for (block, h) in self.blocks.iter().zip(state.iter_mut()) {
517            // GRU over time, one independent state per frequency token.
518            for fr in 0..RF_FREQ {
519                let tok = &mut tokens[fr * RF_CH..(fr + 1) * RF_CH];
520                let hcur = &mut h[fr * RF_CH..(fr + 1) * RF_CH];
521                let mut gi = [0.0f32; 3 * RF_CH];
522                let mut gh = [0.0f32; 3 * RF_CH];
523                for g in 0..3 * RF_CH {
524                    let wi = &block.rnn.weight_ih[g * RF_CH..(g + 1) * RF_CH];
525                    let wh = &block.rnn.weight_hh[g * RF_CH..(g + 1) * RF_CH];
526                    let mut ai = block.rnn.bias_ih[g];
527                    let mut ah = block.rnn.bias_hh[g];
528                    for c in 0..RF_CH {
529                        ai += wi[c] * tok[c];
530                        ah += wh[c] * hcur[c];
531                    }
532                    gi[g] = ai;
533                    gh[g] = ah;
534                }
535                let mut rnn_out = [0.0f32; RF_CH];
536                for c in 0..RF_CH {
537                    let r = sigmoid(gi[c] + gh[c]);
538                    let z = sigmoid(gi[RF_CH + c] + gh[RF_CH + c]);
539                    let ncand = (gi[2 * RF_CH + c] + r * gh[2 * RF_CH + c]).tanh();
540                    let hnew = (1.0 - z) * ncand + z * hcur[c];
541                    hcur[c] = hnew;
542                    rnn_out[c] = hnew;
543                }
544                // rnn_fc (post-norm folded into weight+bias) + residual.
545                for (c, slot) in tok.iter_mut().enumerate().take(RF_CH) {
546                    let w = &block.rnn_fc.weight[c * RF_CH..(c + 1) * RF_CH];
547                    let mut acc = block.rnn_fc.bias[c];
548                    for i in 0..RF_CH {
549                        acc += w[i] * rnn_out[i];
550                    }
551                    *slot += acc;
552                }
553            }
554
555            if let Some(pe) = &block.pe {
556                for (slot, p) in tokens.iter_mut().zip(pe.iter()) {
557                    *slot += p;
558                }
559            }
560
561            // Frequency attention over RF_FREQ tokens.
562            let mut qkv = vec![0.0f32; RF_FREQ * 3 * RF_CH];
563            for fr in 0..RF_FREQ {
564                let tok = &tokens[fr * RF_CH..(fr + 1) * RF_CH];
565                for o in 0..3 * RF_CH {
566                    let w = &block.qkv.weight[o * RF_CH..(o + 1) * RF_CH];
567                    let mut acc = 0.0f32;
568                    for c in 0..RF_CH {
569                        acc += w[c] * tok[c];
570                    }
571                    qkv[fr * 3 * RF_CH + o] = acc;
572                }
573            }
574            let scale = 1.0 / (HEAD_DIM as f32).sqrt();
575            let mut attn_out = vec![0.0f32; RF_FREQ * RF_CH];
576            let mut scores = [0.0f32; RF_FREQ];
577            for head in 0..HEADS {
578                // The reference reshapes qkv to [.., heads, 3*HEAD_DIM]: head h owns
579                // contiguous columns [h*3D .. (h+1)*3D] split q/k/v inside.
580                let base = head * 3 * HEAD_DIM;
581                for i in 0..RF_FREQ {
582                    let q = &qkv[i * 3 * RF_CH + base..i * 3 * RF_CH + base + HEAD_DIM];
583                    let mut max = f32::NEG_INFINITY;
584                    for (j, s) in scores.iter_mut().enumerate() {
585                        let k = &qkv
586                            [j * 3 * RF_CH + base + HEAD_DIM..j * 3 * RF_CH + base + 2 * HEAD_DIM];
587                        let mut acc = 0.0f32;
588                        for d in 0..HEAD_DIM {
589                            acc += q[d] * k[d];
590                        }
591                        *s = acc * scale;
592                        max = max.max(*s);
593                    }
594                    let mut denom = 0.0f32;
595                    for s in scores.iter_mut() {
596                        *s = (*s - max).exp();
597                        denom += *s;
598                    }
599                    let inv = 1.0 / denom;
600                    let out = &mut attn_out
601                        [i * RF_CH + head * HEAD_DIM..i * RF_CH + (head + 1) * HEAD_DIM];
602                    for (j, s) in scores.iter().enumerate() {
603                        let v = &qkv[j * 3 * RF_CH + base + 2 * HEAD_DIM
604                            ..j * 3 * RF_CH + base + 3 * HEAD_DIM];
605                        let p = *s * inv;
606                        for d in 0..HEAD_DIM {
607                            out[d] += p * v[d];
608                        }
609                    }
610                }
611            }
612            for fr in 0..RF_FREQ {
613                let src = &attn_out[fr * RF_CH..(fr + 1) * RF_CH];
614                let tok = &mut tokens[fr * RF_CH..(fr + 1) * RF_CH];
615                for (c, slot) in tok.iter_mut().enumerate() {
616                    let w = &block.attn_fc.weight[c * RF_CH..(c + 1) * RF_CH];
617                    let mut acc = block.attn_fc.bias[c];
618                    for i in 0..RF_CH {
619                        acc += w[i] * src[i];
620                    }
621                    *slot += acc;
622                }
623            }
624        }
625
626        // ---- RNNFormer postnet: back to [CH][F_ENC] --------------------------------------
627        // tokens [RF_FREQ][RF_CH] -> per channel freq expansion, then 1x1 mix RF_CH -> CH.
628        let mut yf = vec![0.0f32; RF_CH * F_ENC];
629        for c in 0..RF_CH {
630            for f in 0..F_ENC {
631                let w = &self.rf_post_lin.weight[f * RF_FREQ..(f + 1) * RF_FREQ];
632                let mut acc = 0.0f32;
633                for fr in 0..RF_FREQ {
634                    acc += w[fr] * tokens[fr * RF_CH + c];
635                }
636                yf[c * F_ENC + f] = acc;
637            }
638        }
639        let mut y = vec![0.0f32; CH * F_ENC];
640        for oc in 0..CH {
641            let w = &self.rf_post_conv.weight[oc * RF_CH..(oc + 1) * RF_CH];
642            let b = self.rf_post_conv.bias[oc];
643            for f in 0..F_ENC {
644                let mut acc = b;
645                for ic in 0..RF_CH {
646                    acc += w[ic] * yf[ic * F_ENC + f];
647                }
648                y[oc * F_ENC + f] = acc;
649            }
650        }
651
652        // ---- decoder with encoder skips ---------------------------------------------------
653        for (mix, conv) in &self.decoder {
654            let skip = skips.pop().expect("one skip per decoder stage");
655            y = concat_mix(mix, &y, &skip, F_ENC);
656            y = conv_k_same(conv, &y, F_ENC, true);
657        }
658
659        // ---- decoder postnet: 1x1 mix, SiLU, transposed conv to the mask ------------------
660        let skip = skips.pop().expect("enc_pre skip");
661        let z = concat_mix(&self.dec_post_conv, &y, &skip, F_ENC);
662        let mut mask = [0.0f32; 2 * FREQ];
663        // ConvTranspose1d(CH -> 2, k=K0, stride=STRIDE, padding=pad):
664        // out[o][p] = bias[o] + sum_{c, j, m : j*STRIDE + m - pad == p} w[c][o][m] * z[c][j]
665        let pad = (K0 - STRIDE) / 2;
666        for f in 0..FREQ {
667            mask[2 * f] = self.dec_post_up_bias[0];
668            mask[2 * f + 1] = self.dec_post_up_bias[1];
669        }
670        for c in 0..CH {
671            let wrow = &self.dec_post_up[c * 2 * K0..(c + 1) * 2 * K0];
672            for j in 0..F_ENC {
673                let zv = z[c * F_ENC + j];
674                if zv == 0.0 {
675                    continue;
676                }
677                let base = j * STRIDE;
678                for m in 0..K0 {
679                    let p = base + m;
680                    if p < pad || p - pad >= FREQ {
681                        continue;
682                    }
683                    let p = p - pad;
684                    mask[2 * p] += wrow[m] * zv;
685                    mask[2 * p + 1] += wrow[K0 + m] * zv;
686                }
687            }
688        }
689        mask
690    }
691}
692
693/// Windowed-sinc (Lanczos-6) rate conversion, the same kernel enrollment trusts for its
694/// any-rate references: cutoff clamped to the lower Nyquist, taps normalized by their own
695/// sum so DC gain stays 1 at the clip edges.
696#[must_use]
697pub fn resample_lanczos6(mono: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {
698    if from_rate == to_rate {
699        return mono.to_vec();
700    }
701    const LOBES: f64 = 6.0;
702    let ratio = f64::from(to_rate) / f64::from(from_rate);
703    let cutoff = ratio.min(1.0);
704    let half = (LOBES / cutoff).ceil() as isize;
705    let out_len = ((mono.len() as f64) * ratio).round() as usize;
706
707    let mut out = Vec::with_capacity(out_len);
708    for index in 0..out_len {
709        let center = index as f64 / ratio;
710        let first = center.floor() as isize - half + 1;
711        let mut acc = 0.0_f64;
712        let mut norm = 0.0_f64;
713        for tap in first..first + 2 * half {
714            if tap < 0 {
715                continue;
716            }
717            let Some(sample) = mono.get(tap as usize) else {
718                break;
719            };
720            let weight = lanczos6_tap(center - tap as f64, cutoff);
721            acc += weight * f64::from(*sample);
722            norm += weight;
723        }
724        out.push(if norm.abs() > 1e-12 {
725            (acc / norm) as f32
726        } else {
727            0.0
728        });
729    }
730    out
731}
732
733fn lanczos6_tap(distance: f64, cutoff: f64) -> f64 {
734    const LOBES: f64 = 6.0;
735    let x = distance * cutoff;
736    if x.abs() >= LOBES {
737        return 0.0;
738    }
739    let sinc = |v: f64| {
740        if v.abs() < 1e-12 {
741            1.0
742        } else {
743            let p = std::f64::consts::PI * v;
744            p.sin() / p
745        }
746    };
747    sinc(x) * sinc(x / LOBES)
748}
749
750/// Same-padding k-wide conv over `width` positions, optional SiLU.
751fn conv_k_same(conv: &Conv1d, x: &[f32], width: usize, act: bool) -> Vec<f32> {
752    let mut out = vec![0.0f32; conv.out_ch * width];
753    let pad = (conv.k - 1) / 2;
754    for o in 0..conv.out_ch {
755        let orow = &mut out[o * width..(o + 1) * width];
756        for slot in orow.iter_mut() {
757            *slot = conv.bias[o];
758        }
759        for c in 0..conv.in_ch {
760            let w = &conv.weight[(o * conv.in_ch + c) * conv.k..(o * conv.in_ch + c + 1) * conv.k];
761            let xrow = &x[c * width..(c + 1) * width];
762            for (m, &wv) in w.iter().enumerate() {
763                let shift = m as isize - pad as isize;
764                let (dst_start, src_start) = if shift < 0 {
765                    ((-shift) as usize, 0usize)
766                } else {
767                    (0usize, shift as usize)
768                };
769                let count = width - dst_start.max(src_start);
770                for i in 0..count {
771                    orow[dst_start + i] += wv * xrow[src_start + i];
772                }
773            }
774        }
775        if act {
776            for slot in orow.iter_mut() {
777                *slot = silu(*slot);
778            }
779        }
780    }
781    out
782}
783
784/// 1x1 conv over the channel concat `[x ; skip]`, then SiLU.
785fn concat_mix(conv: &Conv1d, x: &[f32], skip: &[f32], width: usize) -> Vec<f32> {
786    let half = conv.in_ch / 2;
787    let mut out = vec![0.0f32; conv.out_ch * width];
788    for o in 0..conv.out_ch {
789        let w = &conv.weight[o * conv.in_ch..(o + 1) * conv.in_ch];
790        let orow = &mut out[o * width..(o + 1) * width];
791        for slot in orow.iter_mut() {
792            *slot = conv.bias[o];
793        }
794        for c in 0..half {
795            let wv = w[c];
796            let xrow = &x[c * width..(c + 1) * width];
797            for (slot, xv) in orow.iter_mut().zip(xrow.iter()) {
798                *slot += wv * xv;
799            }
800        }
801        for c in 0..half {
802            let wv = w[half + c];
803            let srow = &skip[c * width..(c + 1) * width];
804            for (slot, sv) in orow.iter_mut().zip(srow.iter()) {
805                *slot += wv * sv;
806            }
807        }
808        for slot in orow.iter_mut() {
809            *slot = silu(*slot);
810        }
811    }
812    out
813}
814
815// ---------------------------------------------------------------------------------------
816// FFT: iterative radix-2, real transforms via the complex core. n = 1024 only in practice
817// but written for any power of two.
818// ---------------------------------------------------------------------------------------
819
820struct Fft {
821    n: usize,
822    /// Twiddles for the forward transform: e^{-2Ï€ik/n} for k in 0..n/2.
823    tw_re: Vec<f32>,
824    tw_im: Vec<f32>,
825    rev: Vec<u32>,
826}
827
828impl Fft {
829    fn new(n: usize) -> Self {
830        assert!(n.is_power_of_two());
831        let mut tw_re = Vec::with_capacity(n / 2);
832        let mut tw_im = Vec::with_capacity(n / 2);
833        for k in 0..n / 2 {
834            let ang = -2.0 * std::f64::consts::PI * k as f64 / n as f64;
835            tw_re.push(ang.cos() as f32);
836            tw_im.push(ang.sin() as f32);
837        }
838        let bits = n.trailing_zeros();
839        let rev = (0..n as u32)
840            .map(|i| i.reverse_bits() >> (32 - bits))
841            .collect();
842        Self {
843            n,
844            tw_re,
845            tw_im,
846            rev,
847        }
848    }
849
850    /// In-place complex FFT over interleaved (re, im) pairs; `inverse` conjugates the
851    /// twiddles (no 1/n scaling — callers scale).
852    fn fft_complex(&self, buf: &mut [f32], inverse: bool) {
853        let n = self.n;
854        for i in 0..n {
855            let j = self.rev[i] as usize;
856            if i < j {
857                buf.swap(2 * i, 2 * j);
858                buf.swap(2 * i + 1, 2 * j + 1);
859            }
860        }
861        let mut len = 2;
862        while len <= n {
863            let half = len / 2;
864            let step = n / len;
865            let mut start = 0;
866            while start < n {
867                for k in 0..half {
868                    let wre = self.tw_re[k * step];
869                    let wim = if inverse {
870                        -self.tw_im[k * step]
871                    } else {
872                        self.tw_im[k * step]
873                    };
874                    let a = start + k;
875                    let b = a + half;
876                    let (bre, bim) = (buf[2 * b], buf[2 * b + 1]);
877                    let tre = bre * wre - bim * wim;
878                    let tim = bre * wim + bim * wre;
879                    let (are, aim) = (buf[2 * a], buf[2 * a + 1]);
880                    buf[2 * a] = are + tre;
881                    buf[2 * a + 1] = aim + tim;
882                    buf[2 * b] = are - tre;
883                    buf[2 * b + 1] = aim - tim;
884                }
885                start += len;
886            }
887            len *= 2;
888        }
889    }
890
891    /// Real forward transform: `time[n]` -> `spec[2*(n/2+1)]` interleaved.
892    fn rfft(&self, time: &[f32], spec: &mut [f32]) {
893        let n = self.n;
894        let mut buf = vec![0.0f32; 2 * n];
895        for (i, &v) in time.iter().enumerate() {
896            buf[2 * i] = v;
897        }
898        self.fft_complex(&mut buf, false);
899        spec[..2 * (n / 2 + 1)].copy_from_slice(&buf[..2 * (n / 2 + 1)]);
900    }
901
902    /// Inverse real transform: `spec[2*(n/2+1)]` -> `time[n]` (with 1/n scaling).
903    fn irfft(&self, spec: &[f32], time: &mut [f32]) {
904        let n = self.n;
905        let mut buf = vec![0.0f32; 2 * n];
906        buf[..2 * (n / 2 + 1)].copy_from_slice(&spec[..2 * (n / 2 + 1)]);
907        for k in 1..n / 2 {
908            buf[2 * (n - k)] = spec[2 * k];
909            buf[2 * (n - k) + 1] = -spec[2 * k + 1];
910        }
911        self.fft_complex(&mut buf, true);
912        let inv = 1.0 / n as f32;
913        for (i, slot) in time.iter_mut().enumerate() {
914            *slot = buf[2 * i] * inv;
915        }
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922
923    fn zero_weight_enhancer() -> Enhancer {
924        let mut tensors = BTreeMap::new();
925        let mut put = |name: &str, shape: &[usize]| {
926            let count: usize = shape.iter().product();
927            tensors.insert(name.to_owned(), (shape.to_vec(), vec![0.0f32; count]));
928        };
929        put("enc_pre.0.weight", &[CH, 2 * STRIDE, K0 / STRIDE]);
930        put("enc_pre.0.bias", &[CH]);
931        for i in 0..ENC_CONVS {
932            put(&format!("encoder.{i}.0.weight"), &[CH, CH, ENC_K]);
933            put(&format!("encoder.{i}.0.bias"), &[CH]);
934            put(&format!("decoder.{i}.0.weight"), &[CH, 2 * CH, 1]);
935            put(&format!("decoder.{i}.0.bias"), &[CH]);
936            put(&format!("decoder.{i}.2.weight"), &[CH, CH, ENC_K]);
937            put(&format!("decoder.{i}.2.bias"), &[CH]);
938        }
939        put("rf_pre.0.weight", &[RF_FREQ, F_ENC]);
940        put("rf_pre.1.weight", &[RF_CH, CH, 1]);
941        put("rf_pre.1.bias", &[RF_CH]);
942        put("rf_block.0.pe", &[RF_FREQ, RF_CH]);
943        for i in 0..BLOCKS {
944            put(
945                &format!("rf_block.{i}.rnn.weight_ih_l0"),
946                &[3 * RF_CH, RF_CH],
947            );
948            put(
949                &format!("rf_block.{i}.rnn.weight_hh_l0"),
950                &[3 * RF_CH, RF_CH],
951            );
952            put(&format!("rf_block.{i}.rnn.bias_ih_l0"), &[3 * RF_CH]);
953            put(&format!("rf_block.{i}.rnn.bias_hh_l0"), &[3 * RF_CH]);
954            put(&format!("rf_block.{i}.rnn_fc.weight"), &[RF_CH, RF_CH]);
955            put(&format!("rf_block.{i}.rnn_fc.bias"), &[RF_CH]);
956            put(
957                &format!("rf_block.{i}.attn.qkv.weight"),
958                &[3 * RF_CH, RF_CH],
959            );
960            put(&format!("rf_block.{i}.attn_fc.weight"), &[RF_CH, RF_CH]);
961            put(&format!("rf_block.{i}.attn_fc.bias"), &[RF_CH]);
962        }
963        put("rf_post.0.weight", &[F_ENC, RF_FREQ]);
964        put("rf_post.1.weight", &[CH, RF_CH, 1]);
965        put("rf_post.1.bias", &[CH]);
966        put("dec_post.0.weight", &[CH, 2 * CH, 1]);
967        put("dec_post.0.bias", &[CH]);
968        put("dec_post.2.weight", &[CH, 2, K0]);
969        put("dec_post.2.bias", &[2]);
970        put("buffer.stft.window", &[N_FFT]);
971        Enhancer::load(tensors).expect("all shapes present")
972    }
973
974    /// The reflect-padding walk cannot terminate on 0- or 1-sample input; the guard must
975    /// return the contracted empty answer instead of spinning, and short-but-real input
976    /// must keep its exact length through the 24 kHz round trip.
977    #[test]
978    fn tiny_inputs_terminate_and_keep_their_length() {
979        let enhancer = zero_weight_enhancer();
980        assert!(enhancer.enhance_48k(&[]).is_empty());
981        assert!(enhancer.enhance_48k(&[0.25]).is_empty());
982        assert!(enhancer.enhance_48k(&[0.25; 100]).is_empty());
983        assert!(enhancer.enhance_24k(&[]).is_empty());
984        assert_eq!(enhancer.enhance_24k(&[0.25; 50]).len(), 50);
985        // 2,400 samples = 100 ms: enough to cross several hops without making the
986        // debug-profile suite crawl (the full-length case lives in the parity harness).
987        assert_eq!(enhancer.enhance_24k(&[0.25; 2_400]).len(), 2_400);
988        assert_eq!(enhancer.enhance_48k(&[0.25; 1024]).len(), 1024);
989    }
990
991    #[test]
992    fn fft_round_trip_recovers_the_signal() {
993        let fft = Fft::new(1024);
994        let time: Vec<f32> = (0..1024)
995            .map(|i| (i as f32 * 0.013).sin() + 0.3 * (i as f32 * 0.21).cos())
996            .collect();
997        let mut spec = vec![0.0f32; 2 * 513];
998        let mut back = vec![0.0f32; 1024];
999        fft.rfft(&time, &mut spec);
1000        fft.irfft(&spec, &mut back);
1001        for (a, b) in time.iter().zip(back.iter()) {
1002            assert!((a - b).abs() < 1.0e-4, "{a} vs {b}");
1003        }
1004    }
1005
1006    #[test]
1007    fn fft_matches_the_dft_definition() {
1008        let n = 16;
1009        let fft = Fft::new(n);
1010        let time: Vec<f32> = (0..n).map(|i| (i as f32 * 0.7).sin()).collect();
1011        let mut spec = vec![0.0f32; 2 * (n / 2 + 1)];
1012        fft.rfft(&time, &mut spec);
1013        for k in 0..=n / 2 {
1014            let mut re = 0.0f64;
1015            let mut im = 0.0f64;
1016            for (i, &v) in time.iter().enumerate() {
1017                let ang = -2.0 * std::f64::consts::PI * (k * i) as f64 / n as f64;
1018                re += v as f64 * ang.cos();
1019                im += v as f64 * ang.sin();
1020            }
1021            assert!((spec[2 * k] as f64 - re).abs() < 1.0e-3, "bin {k} re");
1022            assert!((spec[2 * k + 1] as f64 - im).abs() < 1.0e-3, "bin {k} im");
1023        }
1024    }
1025}