Skip to main content

cortiq_engine/
ltxaudio.rs

1//! The LTX-2.5 audio path: the spectrogram VAE decoder and the BigVGAN
2//! vocoder that turns its output into a waveform.
3//!
4//! The transformer denoises sound in the same 48 blocks as the picture, so
5//! the soundtrack arrives as a `[8, T, 16]` latent. Turning that into audio
6//! is two models:
7//!
8//! 1. **The audio VAE decoder** — 2-D convolutions over (time, mel bin) with
9//!    PixelNorm and *height-causal* padding, so a frame never sees the
10//!    future. Two mid blocks, then three levels of three residual blocks
11//!    with a nearest ×2 between them; the first row after each upsample is
12//!    dropped, because the causal padding on the following convolution
13//!    already accounts for it. Out comes a 64-bin log-mel spectrogram.
14//! 2. **BigVGAN v2 with bandwidth extension** — `conv_pre`, six transposed
15//!    convolutions each followed by three anti-aliased multi-receptive-field
16//!    blocks whose outputs are averaged, and `conv_post`. Every activation is
17//!    a SnakeBeta sandwiched between a ×2 sinc upsample and a ×2 sinc
18//!    downsample, which is what keeps the harmonics it generates from
19//!    aliasing. That gives 16 kHz stereo; a second generator predicts a
20//!    residual from its mel spectrogram and adds it to a sinc-resampled copy
21//!    at 48 kHz.
22//!
23//! Everything is f32 throughout: the reference notes that bf16 accumulation
24//! through 108 sequential convolutions costs 40-90 % on spectral metrics.
25
26use crate::pool::Pool;
27use cortiq_core::CmfModel;
28use std::sync::Arc;
29
30fn tensor_f32(model: &Arc<CmfModel>, name: &str) -> Result<(Vec<f32>, Vec<usize>), String> {
31    let e = model
32        .tensor(name)
33        .ok_or_else(|| format!("missing tensor {name}"))?;
34    let mut out = vec![0.0f32; e.n_elems()];
35    cortiq_core::quant::dequant_tensor(e, model.entry_bytes(e), &mut out)?;
36    Ok((out, e.shape.clone()))
37}
38
39fn silu(v: f32) -> f32 {
40    v / (1.0 + (-v).exp())
41}
42
43/// A `[C, H, W]` plane: channels, time, mel bin.
44#[derive(Clone)]
45pub struct Grid {
46    pub c: usize,
47    pub h: usize,
48    pub w: usize,
49    pub data: Vec<f32>,
50}
51
52impl Grid {
53    fn zeros(c: usize, h: usize, w: usize) -> Grid {
54        Grid {
55            c,
56            h,
57            w,
58            data: vec![0.0; c * h * w],
59        }
60    }
61    fn n(&self) -> usize {
62        self.h * self.w
63    }
64}
65
66/// 2-D convolution, padded symmetrically on the mel axis and **causally on
67/// the time axis** — the whole kernel extent on the left, nothing on the
68/// right.
69struct Conv2d {
70    w: Vec<f32>,
71    b: Vec<f32>,
72    c_out: usize,
73    c_in: usize,
74    kh: usize,
75    kw: usize,
76}
77
78impl Conv2d {
79    fn load(model: &Arc<CmfModel>, name: &str) -> Result<Conv2d, String> {
80        let (w, s) = tensor_f32(model, &format!("{name}.weight"))?;
81        let (b, _) = tensor_f32(model, &format!("{name}.bias"))?;
82        Ok(Conv2d {
83            w,
84            b,
85            c_out: s[0],
86            c_in: s[1],
87            kh: s[2],
88            kw: s[3],
89        })
90    }
91
92    fn forward(&self, x: &Grid, pool: Option<&Pool>) -> Grid {
93        let (h, w) = (x.h, x.w);
94        let npos = h * w;
95        let k = self.c_in * self.kh * self.kw;
96        let mut out = Grid::zeros(self.c_out, h, w);
97        let pad_h = self.kh - 1; // causal: all of it on the left
98        let pad_w = (self.kw - 1) / 2;
99        const CHUNK: usize = 8192;
100        let mut patches = vec![0f32; CHUNK.min(npos) * k];
101        let mut ys = vec![0f32; CHUNK.min(npos) * self.c_out];
102        let mut p0 = 0usize;
103        while p0 < npos {
104            let n = CHUNK.min(npos - p0);
105            patches[..n * k].fill(0.0);
106            for i in 0..n {
107                let p = p0 + i;
108                let (pwi, phi) = (p % w, p / w);
109                for ci in 0..self.c_in {
110                    for a in 0..self.kh {
111                        let sh = phi as isize + a as isize - pad_h as isize;
112                        if sh < 0 || sh >= h as isize {
113                            continue;
114                        }
115                        for bb in 0..self.kw {
116                            let sw = pwi as isize + bb as isize - pad_w as isize;
117                            if sw < 0 || sw >= w as isize {
118                                continue;
119                            }
120                            patches[i * k + (ci * self.kh + a) * self.kw + bb] =
121                                x.data[(ci * h + sh as usize) * w + sw as usize];
122                        }
123                    }
124                }
125            }
126            crate::fcd_ops::gemm_nt(
127                &patches[..n * k],
128                &self.w,
129                &mut ys[..n * self.c_out],
130                n,
131                k,
132                self.c_out,
133                pool,
134            );
135            for i in 0..n {
136                for co in 0..self.c_out {
137                    out.data[co * npos + p0 + i] = ys[i * self.c_out + co] + self.b[co];
138                }
139            }
140            p0 += n;
141        }
142        out
143    }
144}
145
146/// RMS across channels at each (time, mel) location — no learned weight.
147fn pixel_norm(x: &mut Grid) {
148    let n = x.n();
149    for p in 0..n {
150        let mut ss = 0f64;
151        for c in 0..x.c {
152            let v = x.data[c * n + p] as f64;
153            ss += v * v;
154        }
155        let inv = 1.0 / (ss / x.c as f64 + 1e-6).sqrt();
156        for c in 0..x.c {
157            x.data[c * n + p] = (x.data[c * n + p] as f64 * inv) as f32;
158        }
159    }
160}
161
162struct ResnetBlock {
163    conv1: Conv2d,
164    conv2: Conv2d,
165    shortcut: Option<Conv2d>,
166}
167
168impl ResnetBlock {
169    fn load(model: &Arc<CmfModel>, p: &str) -> Result<ResnetBlock, String> {
170        Ok(ResnetBlock {
171            conv1: Conv2d::load(model, &format!("{p}.conv1.conv"))?,
172            conv2: Conv2d::load(model, &format!("{p}.conv2.conv"))?,
173            shortcut: match model.tensor(&format!("{p}.nin_shortcut.conv.weight")) {
174                Some(_) => Some(Conv2d::load(model, &format!("{p}.nin_shortcut.conv"))?),
175                None => None,
176            },
177        })
178    }
179
180    fn forward(&self, x: &Grid, pool: Option<&Pool>) -> Grid {
181        let mut h = x.clone();
182        pixel_norm(&mut h);
183        h.data.iter_mut().for_each(|v| *v = silu(*v));
184        let mut h = self.conv1.forward(&h, pool);
185        pixel_norm(&mut h);
186        h.data.iter_mut().for_each(|v| *v = silu(*v));
187        let mut h = self.conv2.forward(&h, pool);
188        let res = match &self.shortcut {
189            Some(c) => c.forward(x, pool),
190            None => x.clone(),
191        };
192        for (v, &r) in h.data.iter_mut().zip(&res.data) {
193            *v += r;
194        }
195        h
196    }
197}
198
199/// Nearest ×2 on both axes, a convolution, then the first time row dropped
200/// — the causal padding on the convolution has already reproduced it.
201fn upsample2(x: &Grid, conv: &Conv2d, pool: Option<&Pool>) -> Grid {
202    let (h2, w2) = (x.h * 2, x.w * 2);
203    let mut up = Grid::zeros(x.c, h2, w2);
204    for c in 0..x.c {
205        for y in 0..h2 {
206            for z in 0..w2 {
207                up.data[(c * h2 + y) * w2 + z] = x.data[(c * x.h + y / 2) * x.w + z / 2];
208            }
209        }
210    }
211    let conved = conv.forward(&up, pool);
212    let mut out = Grid::zeros(conved.c, h2 - 1, w2);
213    for c in 0..conved.c {
214        for y in 1..h2 {
215            for z in 0..w2 {
216                out.data[(c * (h2 - 1) + y - 1) * w2 + z] = conved.data[(c * h2 + y) * w2 + z];
217            }
218        }
219    }
220    out
221}
222
223pub struct AudioVaeDecoder {
224    conv_in: Conv2d,
225    mid: Vec<ResnetBlock>,
226    levels: Vec<(Vec<ResnetBlock>, Option<Conv2d>)>,
227    conv_out: Conv2d,
228    mean: Vec<f32>,
229    std: Vec<f32>,
230}
231
232impl AudioVaeDecoder {
233    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<AudioVaeDecoder, String> {
234        let mut levels = Vec::new();
235        let mut lv = 0usize;
236        while model
237            .tensor(&format!("avae.decoder.up.{lv}.block.0.conv1.conv.weight"))
238            .is_some()
239        {
240            let mut blocks = Vec::new();
241            let mut bi = 0usize;
242            while model
243                .tensor(&format!(
244                    "avae.decoder.up.{lv}.block.{bi}.conv1.conv.weight"
245                ))
246                .is_some()
247            {
248                blocks.push(ResnetBlock::load(
249                    model,
250                    &format!("avae.decoder.up.{lv}.block.{bi}"),
251                )?);
252                bi += 1;
253            }
254            let up = match model.tensor(&format!("avae.decoder.up.{lv}.upsample.conv.conv.weight"))
255            {
256                Some(_) => Some(Conv2d::load(
257                    model,
258                    &format!("avae.decoder.up.{lv}.upsample.conv.conv"),
259                )?),
260                None => None,
261            };
262            levels.push((blocks, up));
263            lv += 1;
264        }
265        Ok(AudioVaeDecoder {
266            conv_in: Conv2d::load(model, "avae.decoder.conv_in.conv")?,
267            mid: vec![
268                ResnetBlock::load(model, "avae.decoder.mid.block_1")?,
269                ResnetBlock::load(model, "avae.decoder.mid.block_2")?,
270            ],
271            levels,
272            conv_out: Conv2d::load(model, "avae.decoder.conv_out.conv")?,
273            mean: tensor_f32(model, "avae.per_channel_statistics.mean-of-means")?.0,
274            std: tensor_f32(model, "avae.per_channel_statistics.std-of-means")?.0,
275        })
276    }
277
278    /// `[8, T, 16]` latent → `[2, 4T-3, 64]` log-mel spectrogram.
279    pub fn decode(&self, latent: &Grid, pool: Option<&Pool>) -> Grid {
280        // The statistics are per *patchified* channel (channel-major over mel
281        // bins), which is how the transformer sees them.
282        let mut x = latent.clone();
283        let n = x.n();
284        for c in 0..x.c {
285            for wi in 0..x.w {
286                let idx = c * x.w + wi;
287                let (m, s) = (self.mean[idx], self.std[idx]);
288                for hi in 0..x.h {
289                    let o = (c * x.h + hi) * x.w + wi;
290                    x.data[o] = x.data[o] * s + m;
291                }
292            }
293        }
294        let _ = n;
295        let mut h = self.conv_in.forward(&x, pool);
296        for b in &self.mid {
297            h = b.forward(&h, pool);
298        }
299        for (blocks, up) in self.levels.iter().rev() {
300            for b in blocks {
301                h = b.forward(&h, pool);
302            }
303            if let Some(c) = up {
304                h = upsample2(&h, c, pool);
305            }
306        }
307        pixel_norm(&mut h);
308        h.data.iter_mut().for_each(|v| *v = silu(*v));
309        let out = self.conv_out.forward(&h, pool);
310        // the causal decoder produces 4T-3 frames; crop to that
311        let target = (latent.h * 4).saturating_sub(3).max(1);
312        if out.h == target {
313            return out;
314        }
315        let mut cropped = Grid::zeros(out.c, target.min(out.h), out.w);
316        for c in 0..out.c {
317            for y in 0..cropped.h {
318                for z in 0..out.w {
319                    cropped.data[(c * cropped.h + y) * out.w + z] =
320                        out.data[(c * out.h + y) * out.w + z];
321                }
322            }
323        }
324        cropped
325    }
326}
327
328// ------------------------------------------------------------- 1-D layers
329
330/// A multi-channel signal `[C, T]`.
331#[derive(Clone)]
332pub struct Sig {
333    pub c: usize,
334    pub t: usize,
335    pub data: Vec<f32>,
336}
337
338impl Sig {
339    fn zeros(c: usize, t: usize) -> Sig {
340        Sig {
341            c,
342            t,
343            data: vec![0.0; c * t],
344        }
345    }
346}
347
348/// `f(channel, row)` over the channels of a `[c][t]` buffer, across the
349/// cores: the vocoder's activations and resampling filters treat every
350/// channel on its own.
351fn par_channels(data: &mut [f32], t: usize, f: &(dyn Fn(usize, &mut [f32]) + Sync)) {
352    if t == 0 || data.is_empty() {
353        return;
354    }
355    let c = data.len() / t;
356    let threads = std::thread::available_parallelism().map_or(1, |n| n.get()).min(c).min(32);
357    if threads <= 1 || data.len() < (1 << 16) {
358        for (i, row) in data.chunks_mut(t).enumerate() {
359            f(i, row);
360        }
361        return;
362    }
363    let per = c.div_ceil(threads);
364    std::thread::scope(|sc| {
365        for (g, chunk) in data.chunks_mut(per * t).enumerate() {
366            sc.spawn(move || {
367                for (i, row) in chunk.chunks_mut(t).enumerate() {
368                    f(g * per + i, row);
369                }
370            });
371        }
372    });
373}
374
375struct Conv1d {
376    w: Vec<f32>,
377    b: Option<Vec<f32>>,
378    c_out: usize,
379    c_in: usize,
380    k: usize,
381    dilation: usize,
382    pad: usize,
383}
384
385impl Conv1d {
386    fn load(model: &Arc<CmfModel>, name: &str, dilation: usize) -> Result<Conv1d, String> {
387        let (w, s) = tensor_f32(model, &format!("{name}.weight"))?;
388        let b = tensor_f32(model, &format!("{name}.bias")).ok().map(|x| x.0);
389        let k = s[2];
390        Ok(Conv1d {
391            w,
392            b,
393            c_out: s[0],
394            c_in: s[1],
395            k,
396            dilation,
397            pad: (k - 1) * dilation / 2,
398        })
399    }
400
401    fn forward(&self, x: &Sig, pool: Option<&Pool>) -> Sig {
402        let t = x.t;
403        let kk = self.c_in * self.k;
404        let mut ys = vec![0f32; t * self.c_out];
405        // The device gathers the columns itself where it has the kernel
406        // (only `x` crosses the bus); the host builds them otherwise.
407        let on_device = std::env::var("CMF_LTX_CONV1D_GPU").as_deref() != Ok("0")
408            && crate::gpu::enabled_here()
409            && crate::gpu::conv1d_gemm(&x.data, &self.w, self.c_in, self.c_out, t, self.k, self.pad, self.dilation, t, &mut ys);
410        if !on_device {
411            let mut patches = vec![0f32; t * kk];
412            for p in 0..t {
413                for ci in 0..self.c_in {
414                    for a in 0..self.k {
415                        let s = p as isize + (a * self.dilation) as isize - self.pad as isize;
416                        if s >= 0 && s < t as isize {
417                            patches[p * kk + ci * self.k + a] = x.data[ci * t + s as usize];
418                        }
419                    }
420                }
421            }
422            crate::fcd_ops::gemm_nt(&patches, &self.w, &mut ys, t, kk, self.c_out, pool);
423        }
424        let mut out = Sig::zeros(self.c_out, t);
425        for p in 0..t {
426            for co in 0..self.c_out {
427                out.data[co * t + p] =
428                    ys[p * self.c_out + co] + self.b.as_ref().map_or(0.0, |b| b[co]);
429            }
430        }
431        out
432    }
433}
434
435/// `ConvTranspose1d(in, out, k, stride, padding)`, weights `[in, out, k]`.
436struct ConvT1d {
437    w: Vec<f32>,
438    b: Option<Vec<f32>>,
439    c_in: usize,
440    c_out: usize,
441    k: usize,
442    stride: usize,
443    pad: usize,
444}
445
446impl ConvT1d {
447    fn load(model: &Arc<CmfModel>, name: &str, stride: usize) -> Result<ConvT1d, String> {
448        let (w, s) = tensor_f32(model, &format!("{name}.weight"))?;
449        let b = tensor_f32(model, &format!("{name}.bias")).ok().map(|x| x.0);
450        let k = s[2];
451        Ok(ConvT1d {
452            w,
453            b,
454            c_in: s[0],
455            c_out: s[1],
456            k,
457            stride,
458            pad: (k - stride) / 2,
459        })
460    }
461
462    fn forward(&self, x: &Sig) -> Sig {
463        let t_out = (x.t - 1) * self.stride + self.k - 2 * self.pad;
464        let mut out = Sig::zeros(self.c_out, t_out);
465        // An output channel at a time, across the cores; each value still
466        // sums its taps in (input channel, position, tap) order.
467        par_channels(&mut out.data, t_out, &|co, row| {
468            for ci in 0..self.c_in {
469                let w = &self.w[(ci * self.c_out + co) * self.k..][..self.k];
470                for (p, &v) in x.data[ci * x.t..(ci + 1) * x.t].iter().enumerate() {
471                    if v == 0.0 {
472                        continue;
473                    }
474                    let base = p * self.stride;
475                    for (a, &wa) in w.iter().enumerate() {
476                        let o = base + a;
477                        if o < self.pad || o - self.pad >= t_out {
478                            continue;
479                        }
480                        row[o - self.pad] += v * wa;
481                    }
482                }
483            }
484            if let Some(b) = &self.b {
485                for v in row.iter_mut() {
486                    *v += b[co];
487                }
488            }
489        });
490        out
491    }
492}
493
494/// The anti-aliasing pair around every activation: a ×2 sinc upsample, the
495/// nonlinearity, a ×2 sinc downsample. Both filters ship in the checkpoint.
496struct Aliasing {
497    up: Vec<f32>,
498    down: Vec<f32>,
499    ratio: usize,
500}
501
502impl Aliasing {
503    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Aliasing, String> {
504        Ok(Aliasing {
505            up: tensor_f32(model, &format!("{p}.upsample.filter"))?.0,
506            down: tensor_f32(model, &format!("{p}.downsample.lowpass.filter"))?.0,
507            ratio: 2,
508        })
509    }
510
511    fn upsample(&self, x: &Sig) -> Sig {
512        let k = self.up.len();
513        let stride = self.ratio;
514        let pad = k / stride - 1;
515        let pad_left = pad * stride + (k - stride) / 2;
516        let pad_right = pad * stride + (k - stride).div_ceil(2);
517        // replicate-pad, transposed convolution, then the same trim the
518        // reference takes
519        let tp = x.t + 2 * pad;
520        let full = (tp - 1) * stride + k;
521        let (lo, hi) = (pad_left, full - pad_right);
522        let t2 = hi - lo;
523        let mut trimmed = Sig::zeros(x.c, t2);
524        par_channels(&mut trimmed.data, t2, &|c, row| {
525            let mut out = vec![0f32; full];
526            for p in 0..tp {
527                let src = (p as isize - pad as isize).clamp(0, x.t as isize - 1) as usize;
528                let v = x.data[c * x.t + src] * self.ratio as f32;
529                if v == 0.0 {
530                    continue;
531                }
532                for a in 0..k {
533                    out[p * stride + a] += v * self.up[a];
534                }
535            }
536            row.copy_from_slice(&out[lo..hi]);
537        });
538        trimmed
539    }
540
541    fn downsample(&self, x: &Sig) -> Sig {
542        let k = self.down.len();
543        let pad_left = k / 2 - if k % 2 == 0 { 1 } else { 0 };
544        let pad_right = k / 2;
545        let tp = x.t + pad_left + pad_right;
546        let t2 = (tp - k) / self.ratio + 1;
547        let mut out = Sig::zeros(x.c, t2);
548        par_channels(&mut out.data, t2, &|c, row| {
549            for (p, o) in row.iter_mut().enumerate() {
550                let mut acc = 0f32;
551                for a in 0..k {
552                    let s = (p * self.ratio + a) as isize - pad_left as isize;
553                    let s = s.clamp(0, x.t as isize - 1) as usize;
554                    acc += x.data[c * x.t + s] * self.down[a];
555                }
556                *o = acc;
557            }
558        });
559        out
560    }
561}
562
563/// `x + sin(αx)² / β`, with α and β kept in log space.
564struct SnakeBeta {
565    alpha: Vec<f32>,
566    beta: Vec<f32>,
567    aa: Aliasing,
568}
569
570impl SnakeBeta {
571    fn load(model: &Arc<CmfModel>, p: &str) -> Result<SnakeBeta, String> {
572        Ok(SnakeBeta {
573            alpha: tensor_f32(model, &format!("{p}.act.alpha"))?.0,
574            beta: tensor_f32(model, &format!("{p}.act.beta"))?.0,
575            aa: Aliasing::load(model, p)?,
576        })
577    }
578
579    fn forward(&self, x: &Sig) -> Sig {
580        let mut up = self.aa.upsample(x);
581        let t = up.t;
582        par_channels(&mut up.data, t, &|c, row| {
583            let a = self.alpha[c].exp();
584            let b = self.beta[c].exp();
585            for v in row.iter_mut() {
586                let s = (*v * a).sin();
587                *v += s * s / (b + 1e-9);
588            }
589        });
590        self.aa.downsample(&up)
591    }
592}
593
594/// One multi-receptive-field block: three dilated conv pairs, each wrapped
595/// in its own anti-aliased activation, summed into the residual.
596struct AmpBlock {
597    convs1: Vec<Conv1d>,
598    convs2: Vec<Conv1d>,
599    acts1: Vec<SnakeBeta>,
600    acts2: Vec<SnakeBeta>,
601}
602
603impl AmpBlock {
604    fn load(model: &Arc<CmfModel>, p: &str, dil: &[usize]) -> Result<AmpBlock, String> {
605        let mut convs1 = Vec::new();
606        let mut convs2 = Vec::new();
607        let mut acts1 = Vec::new();
608        let mut acts2 = Vec::new();
609        for (i, &d) in dil.iter().enumerate() {
610            convs1.push(Conv1d::load(model, &format!("{p}.convs1.{i}"), d)?);
611            convs2.push(Conv1d::load(model, &format!("{p}.convs2.{i}"), 1)?);
612            acts1.push(SnakeBeta::load(model, &format!("{p}.acts1.{i}"))?);
613            acts2.push(SnakeBeta::load(model, &format!("{p}.acts2.{i}"))?);
614        }
615        Ok(AmpBlock {
616            convs1,
617            convs2,
618            acts1,
619            acts2,
620        })
621    }
622
623    fn forward(&self, x: &Sig, pool: Option<&Pool>) -> Sig {
624        let mut x = x.clone();
625        for i in 0..self.convs1.len() {
626            let h = self.acts1[i].forward(&x);
627            let h = self.convs1[i].forward(&h, pool);
628            let h = self.acts2[i].forward(&h);
629            let h = self.convs2[i].forward(&h, pool);
630            for (v, &y) in x.data.iter_mut().zip(&h.data) {
631                *v += y;
632            }
633        }
634        x
635    }
636}
637
638/// BigVGAN v2: `conv_pre`, then per level a transposed convolution and three
639/// receptive-field blocks whose outputs are averaged, then `conv_post`.
640pub struct Vocoder {
641    conv_pre: Conv1d,
642    ups: Vec<ConvT1d>,
643    blocks: Vec<AmpBlock>,
644    per_level: usize,
645    act_post: SnakeBeta,
646    conv_post: Conv1d,
647    tanh_final: bool,
648    apply_final: bool,
649}
650
651impl Vocoder {
652    fn from_cmf(
653        model: &Arc<CmfModel>,
654        p: &str,
655        rates: &[usize],
656        dils: &[Vec<usize>],
657        apply_final: bool,
658    ) -> Result<Vocoder, String> {
659        let mut ups = Vec::new();
660        for (i, &r) in rates.iter().enumerate() {
661            ups.push(ConvT1d::load(model, &format!("{p}.ups.{i}"), r)?);
662        }
663        let mut blocks = Vec::new();
664        let mut i = 0usize;
665        while model
666            .tensor(&format!("{p}.resblocks.{i}.convs1.0.weight"))
667            .is_some()
668        {
669            blocks.push(AmpBlock::load(
670                model,
671                &format!("{p}.resblocks.{i}"),
672                &dils[i % dils.len()],
673            )?);
674            i += 1;
675        }
676        let per_level = blocks.len() / rates.len().max(1);
677        Ok(Vocoder {
678            conv_pre: Conv1d::load(model, &format!("{p}.conv_pre"), 1)?,
679            ups,
680            blocks,
681            per_level,
682            act_post: SnakeBeta::load(model, &format!("{p}.act_post"))?,
683            conv_post: Conv1d::load(model, &format!("{p}.conv_post"), 1)?,
684            tanh_final: false,
685            apply_final,
686        })
687    }
688
689    /// `[2, T, mel]` log-mel → `[2, T·∏rates]` waveform.
690    fn forward(&self, mel: &Grid, pool: Option<&Pool>) -> Sig {
691        // (channels, time, mel) → (channels·mel, time)
692        let mut x = Sig::zeros(mel.c * mel.w, mel.h);
693        for s in 0..mel.c {
694            for m in 0..mel.w {
695                let c = s * mel.w + m;
696                for t in 0..mel.h {
697                    x.data[c * mel.h + t] = mel.data[(s * mel.h + t) * mel.w + m];
698                }
699            }
700        }
701        let dbg = std::env::var("CMF_LTX_VOC_DBG").is_ok();
702        let rms = |name: &str, s: &Sig| {
703            let n = s.data.len().max(1) as f64;
704            let r = (s.data.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>() / n).sqrt();
705            println!("  voc {name:<10} [{}, {}] rms {r:.6}", s.c, s.t);
706        };
707        let mut h = self.conv_pre.forward(&x, pool);
708        if dbg {
709            rms("in", &x);
710            rms("conv_pre", &h);
711        }
712        for (i, up) in self.ups.iter().enumerate() {
713            h = up.forward(&h);
714            let mut acc: Option<Sig> = None;
715            for j in 0..self.per_level {
716                let b = &self.blocks[i * self.per_level + j];
717                let o = b.forward(&h, pool);
718                match &mut acc {
719                    None => acc = Some(o),
720                    Some(a) => {
721                        for (v, &y) in a.data.iter_mut().zip(&o.data) {
722                            *v += y;
723                        }
724                    }
725                }
726            }
727            h = acc.unwrap();
728            let inv = 1.0 / self.per_level as f32;
729            h.data.iter_mut().for_each(|v| *v *= inv);
730            if dbg {
731                rms(&format!("level{i}"), &h);
732            }
733        }
734        let h = self.act_post.forward(&h);
735        let mut out = self.conv_post.forward(&h, pool);
736        if self.apply_final {
737            out.data.iter_mut().for_each(|v| {
738                *v = if self.tanh_final {
739                    v.tanh()
740                } else {
741                    v.clamp(-1.0, 1.0)
742                }
743            });
744        }
745        out
746    }
747}
748
749/// The causal log-mel the bandwidth extender is conditioned on: an STFT
750/// carried out as a convolution with the checkpoint's own DFT × Hann bases,
751/// so the numbers match what the extender was trained against.
752struct MelStft {
753    forward_basis: Vec<f32>,
754    mel_basis: Vec<f32>,
755    n_freqs: usize,
756    filter_len: usize,
757    hop: usize,
758    n_mels: usize,
759}
760
761impl MelStft {
762    fn load(model: &Arc<CmfModel>, p: &str, hop: usize) -> Result<MelStft, String> {
763        let (fb, fs) = tensor_f32(model, &format!("{p}.stft_fn.forward_basis"))?;
764        let (mb, ms) = tensor_f32(model, &format!("{p}.mel_basis"))?;
765        Ok(MelStft {
766            n_freqs: fs[0] / 2,
767            filter_len: fs[2],
768            forward_basis: fb,
769            mel_basis: mb,
770            n_mels: ms[0],
771            hop,
772        })
773    }
774
775    /// `[C, T]` waveform → `[C, frames, n_mels]` log-mel.
776    fn forward(&self, x: &Sig) -> Grid {
777        let left = self.filter_len.saturating_sub(self.hop);
778        let padded = x.t + left;
779        let frames = if padded >= self.filter_len {
780            (padded - self.filter_len) / self.hop + 1
781        } else {
782            0
783        };
784        let mut out = Grid::zeros(x.c, frames, self.n_mels);
785        let rows = 2 * self.n_freqs;
786        let mut mag = vec![0f32; self.n_freqs];
787        for c in 0..x.c {
788            for f in 0..frames {
789                let start = f * self.hop;
790                for r in 0..self.n_freqs {
791                    let mut re = 0f32;
792                    let mut im = 0f32;
793                    for j in 0..self.filter_len {
794                        let idx = start + j;
795                        let v = if idx < left {
796                            0.0
797                        } else {
798                            let s = idx - left;
799                            if s < x.t { x.data[c * x.t + s] } else { 0.0 }
800                        };
801                        re += v * self.forward_basis[r * self.filter_len + j];
802                        im += v * self.forward_basis[(self.n_freqs + r) * self.filter_len + j];
803                    }
804                    mag[r] = (re * re + im * im).sqrt();
805                }
806                for m in 0..self.n_mels {
807                    let mut acc = 0f32;
808                    for r in 0..self.n_freqs {
809                        acc += self.mel_basis[m * self.n_freqs + r] * mag[r];
810                    }
811                    out.data[(c * frames + f) * self.n_mels + m] = acc.max(1e-5).ln();
812                }
813            }
814        }
815        let _ = rows;
816        out
817    }
818}
819
820/// A Hann-windowed sinc resampler, the ×3 skip path from 16 kHz to 48 kHz.
821/// The reference does not store this filter, so it is rebuilt here.
822fn hann_sinc_upsample(x: &Sig, ratio: usize) -> Sig {
823    let rolloff = 0.99f64;
824    let lpw = 6f64;
825    let width = (lpw / rolloff).ceil() as usize;
826    let k = 2 * width * ratio + 1;
827    let pad = width;
828    let pad_left = 2 * width * ratio;
829    let pad_right = k - ratio;
830    let filt: Vec<f32> = (0..k)
831        .map(|i| {
832            let ta = (i as f64 / ratio as f64 - width as f64) * rolloff;
833            let tc = ta.clamp(-lpw, lpw);
834            let win = (tc * std::f64::consts::PI / lpw / 2.0).cos().powi(2);
835            let s = if ta == 0.0 {
836                1.0
837            } else {
838                (std::f64::consts::PI * ta).sin() / (std::f64::consts::PI * ta)
839            };
840            (s * win * rolloff / ratio as f64) as f32
841        })
842        .collect();
843    let tp = x.t + 2 * pad;
844    let full = (tp - 1) * ratio + k;
845    let mut acc = Sig::zeros(x.c, full);
846    for c in 0..x.c {
847        for p in 0..tp {
848            let src = (p as isize - pad as isize).clamp(0, x.t as isize - 1) as usize;
849            let v = x.data[c * x.t + src] * ratio as f32;
850            if v == 0.0 {
851                continue;
852            }
853            for a in 0..k {
854                acc.data[c * full + p * ratio + a] += v * filt[a];
855            }
856        }
857    }
858    let (lo, hi) = (pad_left, full - pad_right);
859    let t2 = hi - lo;
860    let mut out = Sig::zeros(x.c, t2);
861    for c in 0..x.c {
862        out.data[c * t2..(c + 1) * t2].copy_from_slice(&acc.data[c * full + lo..c * full + hi]);
863    }
864    out
865}
866
867/// The whole audio tail: latent → spectrogram → 16 kHz → 48 kHz.
868pub struct AudioStack {
869    pub decoder: AudioVaeDecoder,
870    vocoder: Vocoder,
871    bwe: Vocoder,
872    mel: MelStft,
873    hop: usize,
874    in_rate: usize,
875    pub out_rate: usize,
876}
877
878impl AudioStack {
879    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<AudioStack, String> {
880        let cfg: serde_json::Value = ["avae.config_json"]
881            .iter()
882            .filter_map(|n| model.tensor(n).map(|e| model.entry_bytes(e)))
883            .filter_map(|b| serde_json::from_slice(b).ok())
884            .next()
885            .unwrap_or(serde_json::Value::Null);
886        let voc = cfg.pointer("/vocoder/vocoder").cloned().unwrap_or_default();
887        let bwe = cfg.pointer("/vocoder/bwe").cloned().unwrap_or_default();
888        let rates = |v: &serde_json::Value, d: Vec<usize>| -> Vec<usize> {
889            v.get("upsample_rates")
890                .and_then(|a| a.as_array())
891                .map(|a| {
892                    a.iter()
893                        .filter_map(|x| x.as_u64())
894                        .map(|x| x as usize)
895                        .collect()
896                })
897                .unwrap_or(d)
898        };
899        let dils = vec![vec![1usize, 3, 5], vec![1, 3, 5], vec![1, 3, 5]];
900        let hop = bwe.get("hop_length").and_then(|v| v.as_u64()).unwrap_or(80) as usize;
901        Ok(AudioStack {
902            decoder: AudioVaeDecoder::from_cmf(model)?,
903            // The first generator clamps its output; the bandwidth extender
904            // does not, because its output is a residual that is added to a
905            // resampled copy of the first one and clamped after the sum.
906            vocoder: Vocoder::from_cmf(
907                model,
908                "avae.vocoder.vocoder",
909                &rates(&voc, vec![5, 2, 2, 2, 2, 2]),
910                &dils,
911                voc.get("apply_final_activation")
912                    .and_then(|v| v.as_bool())
913                    .unwrap_or(true),
914            )?,
915            bwe: Vocoder::from_cmf(
916                model,
917                "avae.vocoder.bwe_generator",
918                &rates(&bwe, vec![6, 5, 2, 2, 2]),
919                &dils,
920                bwe.get("apply_final_activation")
921                    .and_then(|v| v.as_bool())
922                    .unwrap_or(true),
923            )?,
924            mel: MelStft::load(model, "avae.vocoder.mel_stft", hop)?,
925            hop,
926            in_rate: bwe
927                .get("input_sampling_rate")
928                .and_then(|v| v.as_u64())
929                .unwrap_or(16000) as usize,
930            out_rate: bwe
931                .get("output_sampling_rate")
932                .and_then(|v| v.as_u64())
933                .unwrap_or(48000) as usize,
934        })
935    }
936
937    /// `[8, T, 16]` latent → stereo waveform at `out_rate`.
938    pub fn decode(&self, latent: &Grid, pool: Option<&Pool>) -> Sig {
939        let mel = self.decoder.decode(latent, pool);
940        self.decode_from_mel(&mel, pool)
941    }
942
943    /// The vocoder half on its own, from a `[2, frames, mel]` log-mel.
944    pub fn decode_from_mel(&self, mel: &Grid, pool: Option<&Pool>) -> Sig {
945        let low = self.vocoder.forward(mel, pool);
946        // the 16 kHz stage on its own, for bisecting the two generators
947        if let Ok(p) = std::env::var("CMF_LTX_LOW_WAV") {
948            let _ = write_wav(std::path::Path::new(&p), &low, self.in_rate);
949        }
950        let out_len = low.t * self.out_rate / self.in_rate;
951        // pad to a whole number of hops so the mel frame count is exact
952        let rem = low.t % self.hop;
953        let padded = if rem == 0 {
954            low.clone()
955        } else {
956            let t2 = low.t + self.hop - rem;
957            let mut p = Sig::zeros(low.c, t2);
958            for c in 0..low.c {
959                p.data[c * t2..c * t2 + low.t]
960                    .copy_from_slice(&low.data[c * low.t..(c + 1) * low.t]);
961            }
962            p
963        };
964        let m = self.mel.forward(&padded);
965        let residual = self.bwe.forward(&m, pool);
966        let skip = hann_sinc_upsample(&padded, self.out_rate / self.in_rate);
967        let t = residual.t.min(skip.t).min(out_len);
968        let mut out = Sig::zeros(skip.c, t);
969        for c in 0..skip.c {
970            for i in 0..t {
971                out.data[c * t + i] = (residual.data[c * residual.t + i]
972                    + skip.data[c * skip.t + i])
973                    .clamp(-1.0, 1.0);
974            }
975        }
976        out
977    }
978}
979
980/// 16-bit PCM WAV — the one container every tool and browser reads.
981pub fn write_wav(path: &std::path::Path, sig: &Sig, rate: usize) -> std::io::Result<()> {
982    use std::io::Write;
983    let n = sig.t;
984    let ch = sig.c as u16;
985    let bytes = (n * sig.c * 2) as u32;
986    let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
987    f.write_all(b"RIFF")?;
988    f.write_all(&(36 + bytes).to_le_bytes())?;
989    f.write_all(b"WAVEfmt ")?;
990    f.write_all(&16u32.to_le_bytes())?;
991    f.write_all(&1u16.to_le_bytes())?;
992    f.write_all(&ch.to_le_bytes())?;
993    f.write_all(&(rate as u32).to_le_bytes())?;
994    f.write_all(&((rate * sig.c * 2) as u32).to_le_bytes())?;
995    f.write_all(&((sig.c * 2) as u16).to_le_bytes())?;
996    f.write_all(&16u16.to_le_bytes())?;
997    f.write_all(b"data")?;
998    f.write_all(&bytes.to_le_bytes())?;
999    for i in 0..n {
1000        for c in 0..sig.c {
1001            let v = (sig.data[c * n + i].clamp(-1.0, 1.0) * 32767.0) as i16;
1002            f.write_all(&v.to_le_bytes())?;
1003        }
1004    }
1005    Ok(())
1006}
1007
1008// ------------------------------------------------------------ the encoder
1009
1010/// A slaney mel filterbank — the one `torchaudio.transforms.MelSpectrogram`
1011/// builds with `mel_scale="slaney", norm="slaney"`. Not in the checkpoint,
1012/// so it is rebuilt from the same formula the reference's preprocessing used.
1013fn mel_filterbank(sr: f64, n_fft: usize, n_mels: usize, fmin: f64, fmax: f64) -> Vec<f32> {
1014    let n_freqs = n_fft / 2 + 1;
1015    let hz_to_mel = |f: f64| 3.0 * f / 200.0;
1016    let mel_to_hz = |m: f64| m * 200.0 / 3.0;
1017    // slaney is linear below 1 kHz and logarithmic above it
1018    let (f_min_log, min_log_mel) = (1000.0f64, 15.0f64);
1019    let logstep = (6.4f64).ln() / 27.0;
1020    let hz_to_mel_s = |f: f64| {
1021        if f >= f_min_log {
1022            min_log_mel + (f / f_min_log).ln() / logstep
1023        } else {
1024            hz_to_mel(f)
1025        }
1026    };
1027    let mel_to_hz_s = |m: f64| {
1028        if m >= min_log_mel {
1029            f_min_log * ((m - min_log_mel) * logstep).exp()
1030        } else {
1031            mel_to_hz(m)
1032        }
1033    };
1034    let (m0, m1) = (hz_to_mel_s(fmin), hz_to_mel_s(fmax));
1035    let pts: Vec<f64> = (0..n_mels + 2)
1036        .map(|i| mel_to_hz_s(m0 + (m1 - m0) * i as f64 / (n_mels + 1) as f64))
1037        .collect();
1038    let freqs: Vec<f64> = (0..n_freqs).map(|i| sr * i as f64 / n_fft as f64).collect();
1039    let mut fb = vec![0f32; n_mels * n_freqs];
1040    for m in 0..n_mels {
1041        let (lo, ctr, hi) = (pts[m], pts[m + 1], pts[m + 2]);
1042        // slaney normalization: unit area per filter
1043        let enorm = 2.0 / (hi - lo);
1044        for (k, &f) in freqs.iter().enumerate() {
1045            let v = if f >= lo && f <= ctr {
1046                (f - lo) / (ctr - lo).max(1e-12)
1047            } else if f > ctr && f <= hi {
1048                (hi - f) / (hi - ctr).max(1e-12)
1049            } else {
1050                0.0
1051            };
1052            fb[m * n_freqs + k] = (v * enorm) as f32;
1053        }
1054    }
1055    fb
1056}
1057
1058/// Waveform → log-mel in the layout the audio VAE encodes: `[C, frames, mel]`.
1059/// Centered STFT with a Hann window and reflect padding, magnitude (not
1060/// power), then the mel projection and a log with the reference's floor.
1061pub fn waveform_to_mel(x: &Sig, sr: usize, n_fft: usize, hop: usize, n_mels: usize) -> Grid {
1062    let n_freqs = n_fft / 2 + 1;
1063    let fb = mel_filterbank(sr as f64, n_fft, n_mels, 0.0, sr as f64 / 2.0);
1064    let win: Vec<f32> = (0..n_fft)
1065        .map(|i| {
1066            let a = std::f64::consts::PI * 2.0 * i as f64 / n_fft as f64;
1067            (0.5 - 0.5 * a.cos()) as f32
1068        })
1069        .collect();
1070    let pad = n_fft / 2;
1071    let frames = x.t / hop + 1;
1072    let mut out = Grid::zeros(x.c, frames, n_mels);
1073    let mut re = vec![0f32; n_freqs];
1074    let mut im = vec![0f32; n_freqs];
1075    for c in 0..x.c {
1076        for f in 0..frames {
1077            let start = f as isize * hop as isize - pad as isize;
1078            re.iter_mut().for_each(|v| *v = 0.0);
1079            im.iter_mut().for_each(|v| *v = 0.0);
1080            for j in 0..n_fft {
1081                // reflect padding at both ends
1082                let mut s = start + j as isize;
1083                if s < 0 {
1084                    s = -s;
1085                }
1086                if s >= x.t as isize {
1087                    s = 2 * (x.t as isize - 1) - s;
1088                }
1089                let v = if s >= 0 && s < x.t as isize {
1090                    x.data[c * x.t + s as usize]
1091                } else {
1092                    0.0
1093                };
1094                let v = v * win[j];
1095                if v == 0.0 {
1096                    continue;
1097                }
1098                for (k, (rr, ii)) in re.iter_mut().zip(im.iter_mut()).enumerate() {
1099                    let a = -2.0 * std::f64::consts::PI * (k * j) as f64 / n_fft as f64;
1100                    *rr += v * a.cos() as f32;
1101                    *ii += v * a.sin() as f32;
1102                }
1103            }
1104            for m in 0..n_mels {
1105                let mut acc = 0f32;
1106                for k in 0..n_freqs {
1107                    acc += fb[m * n_freqs + k] * (re[k] * re[k] + im[k] * im[k]).sqrt();
1108                }
1109                out.data[(c * frames + f) * n_mels + m] = acc.max(1e-5).ln();
1110            }
1111        }
1112    }
1113    out
1114}
1115
1116struct Downsample2 {
1117    conv: Conv2d,
1118}
1119
1120impl Downsample2 {
1121    /// Stride-2 convolution with the encoder's asymmetric padding: two rows
1122    /// of history on the causal (time) axis, one column on the right.
1123    fn forward(&self, x: &Grid, pool: Option<&Pool>) -> Grid {
1124        let (h, w) = (x.h + 2, x.w + 1);
1125        let mut p = Grid::zeros(x.c, h, w);
1126        for c in 0..x.c {
1127            for y in 0..x.h {
1128                for z in 0..x.w {
1129                    p.data[(c * h + y + 2) * w + z] = x.data[(c * x.h + y) * x.w + z];
1130                }
1131            }
1132        }
1133        self.conv.forward_strided(&p, 2, pool)
1134    }
1135}
1136
1137impl Conv2d {
1138    /// The same convolution with an explicit stride and *no* padding of its
1139    /// own — the caller has already padded.
1140    fn forward_strided(&self, x: &Grid, stride: usize, pool: Option<&Pool>) -> Grid {
1141        let (oh, ow) = ((x.h - self.kh) / stride + 1, (x.w - self.kw) / stride + 1);
1142        let npos = oh * ow;
1143        let k = self.c_in * self.kh * self.kw;
1144        let mut patches = vec![0f32; npos * k];
1145        for i in 0..npos {
1146            let (pw, ph) = (i % ow, i / ow);
1147            for ci in 0..self.c_in {
1148                for a in 0..self.kh {
1149                    for b in 0..self.kw {
1150                        patches[i * k + (ci * self.kh + a) * self.kw + b] =
1151                            x.data[(ci * x.h + ph * stride + a) * x.w + pw * stride + b];
1152                    }
1153                }
1154            }
1155        }
1156        let mut ys = vec![0f32; npos * self.c_out];
1157        crate::fcd_ops::gemm_nt(&patches, &self.w, &mut ys, npos, k, self.c_out, pool);
1158        let mut out = Grid::zeros(self.c_out, oh, ow);
1159        for i in 0..npos {
1160            for co in 0..self.c_out {
1161                out.data[co * npos + i] = ys[i * self.c_out + co] + self.b[co];
1162            }
1163        }
1164        out
1165    }
1166}
1167
1168/// The audio VAE's encoder half: log-mel in, latent out.
1169pub struct AudioVaeEncoder {
1170    conv_in: Conv2d,
1171    levels: Vec<(Vec<ResnetBlock>, Option<Downsample2>)>,
1172    mid: Vec<ResnetBlock>,
1173    conv_out: Conv2d,
1174    mean: Vec<f32>,
1175    std: Vec<f32>,
1176    z: usize,
1177}
1178
1179impl AudioVaeEncoder {
1180    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<AudioVaeEncoder, String> {
1181        let mut levels = Vec::new();
1182        let mut lv = 0usize;
1183        while model
1184            .tensor(&format!("avae.encoder.down.{lv}.block.0.conv1.conv.weight"))
1185            .is_some()
1186        {
1187            let mut blocks = Vec::new();
1188            let mut bi = 0usize;
1189            while model
1190                .tensor(&format!(
1191                    "avae.encoder.down.{lv}.block.{bi}.conv1.conv.weight"
1192                ))
1193                .is_some()
1194            {
1195                blocks.push(ResnetBlock::load(
1196                    model,
1197                    &format!("avae.encoder.down.{lv}.block.{bi}"),
1198                )?);
1199                bi += 1;
1200            }
1201            let down = match model.tensor(&format!("avae.encoder.down.{lv}.downsample.conv.weight"))
1202            {
1203                // the downsample holds a plain Conv2d, not the causal wrapper
1204                // the residual blocks use, so it is one `.conv` shallower
1205                Some(_) => Some(Downsample2 {
1206                    conv: Conv2d::load(model, &format!("avae.encoder.down.{lv}.downsample.conv"))?,
1207                }),
1208                None => None,
1209            };
1210            levels.push((blocks, down));
1211            lv += 1;
1212        }
1213        let out = Conv2d::load(model, "avae.encoder.conv_out.conv")?;
1214        let z = out.c_out / 2;
1215        Ok(AudioVaeEncoder {
1216            conv_in: Conv2d::load(model, "avae.encoder.conv_in.conv")?,
1217            levels,
1218            mid: vec![
1219                ResnetBlock::load(model, "avae.encoder.mid.block_1")?,
1220                ResnetBlock::load(model, "avae.encoder.mid.block_2")?,
1221            ],
1222            conv_out: out,
1223            mean: tensor_f32(model, "avae.per_channel_statistics.mean-of-means")?.0,
1224            std: tensor_f32(model, "avae.per_channel_statistics.std-of-means")?.0,
1225            z,
1226        })
1227    }
1228
1229    /// `[2, frames, 64]` log-mel → `[8, frames/4, 16]` latent.
1230    pub fn encode(&self, mel: &Grid, pool: Option<&Pool>) -> Grid {
1231        let mut h = self.conv_in.forward(mel, pool);
1232        for (blocks, down) in &self.levels {
1233            for b in blocks {
1234                h = b.forward(&h, pool);
1235            }
1236            if let Some(d) = down {
1237                h = d.forward(&h, pool);
1238            }
1239        }
1240        for b in &self.mid {
1241            h = b.forward(&h, pool);
1242        }
1243        pixel_norm(&mut h);
1244        h.data.iter_mut().for_each(|v| *v = silu(*v));
1245        let out = self.conv_out.forward(&h, pool);
1246        // means only, then the per-channel statistics of the *patchified*
1247        // layout (channel-major over mel bins)
1248        let npos = out.n();
1249        let mut lat = Grid::zeros(self.z, out.h, out.w);
1250        for c in 0..self.z {
1251            for hi in 0..out.h {
1252                for wi in 0..out.w {
1253                    let idx = c * out.w + wi;
1254                    let v = out.data[(c * out.h + hi) * out.w + wi];
1255                    lat.data[(c * out.h + hi) * out.w + wi] = (v - self.mean[idx]) / self.std[idx];
1256                }
1257            }
1258        }
1259        lat
1260    }
1261}
1262
1263/// A 16-bit PCM WAV back into a signal in `[-1, 1]`.
1264pub fn read_wav(path: &std::path::Path) -> Result<(Sig, usize), String> {
1265    let raw = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
1266    if raw.len() < 44 || &raw[..4] != b"RIFF" || &raw[8..12] != b"WAVE" {
1267        return Err(format!("{}: not a RIFF/WAVE file", path.display()));
1268    }
1269    let mut i = 12usize;
1270    let (mut ch, mut rate, mut bits) = (2usize, 48000usize, 16usize);
1271    let mut data: Option<(usize, usize)> = None;
1272    while i + 8 <= raw.len() {
1273        let id = &raw[i..i + 4];
1274        let len = u32::from_le_bytes(raw[i + 4..i + 8].try_into().unwrap()) as usize;
1275        let body = i + 8;
1276        if id == b"fmt " && body + 16 <= raw.len() {
1277            ch = u16::from_le_bytes(raw[body + 2..body + 4].try_into().unwrap()) as usize;
1278            rate = u32::from_le_bytes(raw[body + 4..body + 8].try_into().unwrap()) as usize;
1279            bits = u16::from_le_bytes(raw[body + 14..body + 16].try_into().unwrap()) as usize;
1280        } else if id == b"data" {
1281            data = Some((body, len.min(raw.len() - body)));
1282            break;
1283        }
1284        i = body + len + (len & 1);
1285    }
1286    let (off, len) = data.ok_or_else(|| format!("{}: no data chunk", path.display()))?;
1287    if bits != 16 {
1288        return Err(format!("{}: only 16-bit PCM is read", path.display()));
1289    }
1290    let n = len / 2 / ch.max(1);
1291    let mut sig = Sig::zeros(ch, n);
1292    for i in 0..n {
1293        for c in 0..ch {
1294            let o = off + (i * ch + c) * 2;
1295            let v = i16::from_le_bytes([raw[o], raw[o + 1]]) as f32 / 32768.0;
1296            sig.data[c * n + i] = v;
1297        }
1298    }
1299    Ok((sig, rate))
1300}
1301
1302/// Resample by linear interpolation — good enough for conditioning input,
1303/// which the mel transform is about to smear across 64 bands anyway.
1304pub fn resample(x: &Sig, from: usize, to: usize) -> Sig {
1305    if from == to {
1306        return x.clone();
1307    }
1308    let n = (x.t as f64 * to as f64 / from as f64).round() as usize;
1309    let mut out = Sig::zeros(x.c, n);
1310    for c in 0..x.c {
1311        for i in 0..n {
1312            let p = i as f64 * from as f64 / to as f64;
1313            let j = p.floor() as usize;
1314            let f = (p - j as f64) as f32;
1315            let a = x.data[c * x.t + j.min(x.t - 1)];
1316            let b = x.data[c * x.t + (j + 1).min(x.t - 1)];
1317            out.data[c * n + i] = a + (b - a) * f;
1318        }
1319    }
1320    out
1321}