Skip to main content

cortiq_engine/
audiovae.rs

1//! MiniMax-H3's audio VAE decoder: BigVGAN at 32 kHz, stereo.
2//!
3//! 32 latent channels at 40 frames a second become a waveform at 800
4//! samples a frame, through seven transposed-convolution stages and
5//! their AMP residual blocks. The two stereo channels are two
6//! independent mono passes, which is how the reference batches them.
7//!
8//! The activations are the interesting part and the easy thing to get
9//! subtly wrong: every nonlinearity is wrapped in a 2× kaiser-sinc
10//! upsample, the pointwise SnakeBeta, and a 2× lowpass back down. The
11//! filter is designed here rather than shipped, from the same
12//! `kaiser_sinc_filter1d(cutoff, half_width, 12)` the reference calls,
13//! so it cannot drift out of step with a checkpoint that does not
14//! contain it.
15
16use crate::pool::Pool;
17
18/// Where an audio decode goes (`CMF_AVAE_TIME=1`). The video decoder
19/// went a whole session untuned because nothing measured it; this one
20/// already misled once, when the shape of the per-channel FIR promised
21/// more than the 9.2 s → 8.1 s it delivered.
22pub static AVAE_TIME: [std::sync::atomic::AtomicU64; 6] = [
23    std::sync::atomic::AtomicU64::new(0),
24    std::sync::atomic::AtomicU64::new(0),
25    std::sync::atomic::AtomicU64::new(0),
26    std::sync::atomic::AtomicU64::new(0),
27    std::sync::atomic::AtomicU64::new(0),
28    std::sync::atomic::AtomicU64::new(0),
29];
30
31fn atime_on() -> bool {
32    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
33    *ON.get_or_init(|| std::env::var("CMF_AVAE_TIME").is_ok())
34}
35
36fn atime(slot: usize, t: std::time::Instant) {
37    if atime_on() {
38        AVAE_TIME[slot].fetch_add(
39            t.elapsed().as_micros() as u64,
40            std::sync::atomic::Ordering::Relaxed,
41        );
42    }
43}
44
45/// One line per phase, sorted by cost.
46pub fn avae_time_report() -> Option<String> {
47    if !atime_on() {
48        return None;
49    }
50    const NAMES: [&str; 6] = [
51        "dec_in", "conv_pre", "upsamples", "resblocks", "act_post", "conv_post",
52    ];
53    let mut v: Vec<(u64, &str)> = AVAE_TIME
54        .iter()
55        .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
56        .zip(NAMES)
57        .collect();
58    let total: u64 = v.iter().map(|(u, _)| u).sum();
59    if total == 0 {
60        return None;
61    }
62    v.sort_by(|a, b| b.0.cmp(&a.0));
63    let mut out = format!("audio vae phases (total {:.1} s):\n", total as f64 / 1e6);
64    for (us, name) in v {
65        out.push_str(&format!(
66            "  {name:<12} {:>6.1} s  {:>5.1}%\n",
67            us as f64 / 1e6,
68            100.0 * us as f64 / total as f64
69        ));
70    }
71    Some(out)
72}
73use cortiq_core::CmfModel;
74use std::sync::Arc;
75
76/// Both resampling filters in the alias-free activation are designed at
77/// this kernel length.
78const FILTER_LEN: usize = 12;
79
80struct Conv1d {
81    w: Vec<f32>, // [out, in, k]
82    b: Option<Vec<f32>>,
83    out_ch: usize,
84    in_ch: usize,
85    k: usize,
86    pad: usize,
87    dilation: usize,
88}
89
90impl Conv1d {
91    fn load(model: &Arc<CmfModel>, name: &str, pad: usize, dilation: usize) -> Result<Self, String> {
92        let e = model
93            .tensor(&format!("{name}.weight"))
94            .ok_or_else(|| format!("missing {name}.weight"))?;
95        let w = crate::dit::cmf_f32(model, &format!("{name}.weight"))?;
96        let b = crate::dit::cmf_f32(model, &format!("{name}.bias")).ok();
97        Ok(Self {
98            out_ch: e.shape[0],
99            in_ch: e.shape[1],
100            k: e.shape[2],
101            w,
102            b,
103            pad,
104            dilation,
105        })
106    }
107
108    /// `x` is `[in_ch, n]`; the result is `[out_ch, n]` for the
109    /// paddings used here (all `same`).
110    fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
111        let out_n = (n + 2 * self.pad).saturating_sub(self.dilation * (self.k - 1));
112        let mut out = vec![0f32; self.out_ch * out_n];
113        let ptr = SendPtr(out.as_mut_ptr());
114        // Each (channel, time slice) pair is independent, so time is
115        // tiled until there are enough rows for any pool. Kept because
116        // it is free and bit-identical — but MEASURED SMALL: 8.6 s →
117        // 8.2 s on a stage that is 95.4% these convolutions. That is
118        // the finding. The resblocks are not starved of parallelism,
119        // they are arithmetic: ~8 s of dilated convolution on the CPU
120        // while the card is idle. The next move is the device, not a
121        // finer split — everything else in this render already went
122        // that way, and this is the last host-bound stage left.
123        //
124        // The shape it wants, so the next pass is implementation and not
125        // research: this convolution IS a GEMM under im2col. Build
126        // `[in_ch·k × out_n]` where row `(i, j)` holds
127        // `x[i, t + j·dilation - pad]` (zero outside), and the weights
128        // are ALREADY `[out_ch × in_ch·k]` in exactly that order —
129        // `w[(o·in_ch + i)·k + j]` — so nothing needs repacking. Then
130        // `out = W · col` with a bias per row. im2col is a gather, which
131        // the card does far better than it does the branch in this loop.
132        //
133        // The f32 device GEMM already exists and is public INSIDE the
134        // backend — `gpu_wgpu::gemm_nt_f32(x, w, y, n, k, m)`, tensor
135        // cores when the card has them. It is simply not re-exported
136        // through `gpu.rs`, which is where every other caller goes. Two
137        // conditions come with it and both suit this decoder: it refuses
138        // under `CMF_BAKE_GPU=0` or strict-f32, and it refuses jobs
139        // below n·k·m = 4M — these convolutions are far above that
140        // (out_ch × in_ch·k × out_n runs to hundreds of millions).
141        // So the work is: a facade re-export, an im2col buffer, and a
142        // parity check against this loop.
143        // im2col + the device GEMM: `out[out_ch × out_n] = W · col`,
144        // with W already `[out_ch × in_ch·k]` in the order this loop
145        // reads it. Opt-in (`CMF_AVAE_CONV_GPU=1`) until measured — the
146        // column buffer is in_ch·k × out_n floats, which is the price.
147        if std::env::var("CMF_AVAE_CONV_GPU").as_deref() != Ok("0")
148            && crate::gpu::enabled_here()
149        {
150            let kk = self.in_ch * self.k;
151            if let Some(col_len) = kk.checked_mul(out_n) {
152                let mut col = vec![0f32; col_len];
153                let pc = SendPtr(col.as_mut_ptr());
154                let fill = |lo: usize, hi: usize| {
155                    for r in lo..hi {
156                        let (i, j) = (r / self.k, r % self.k);
157                        let src = &x[i * n..(i + 1) * n];
158                        // SAFETY: workers own disjoint rows of `col`.
159                        let dst = unsafe { pc.row(r * out_n, out_n) };
160                        for (t, d) in dst.iter_mut().enumerate() {
161                            let p = (t + j * self.dilation) as isize - self.pad as isize;
162                            *d = if p >= 0 && (p as usize) < n {
163                                src[p as usize]
164                            } else {
165                                0.0
166                            };
167                        }
168                    }
169                };
170                match pool {
171                    Some(p) => p.run_rows(kk, &fill),
172                    None => fill(0, kk),
173                }
174                // colᵀ is [out_n × kk]; the GEMM wants x[n×k]·wᵀ[m×k],
175                // so n = out_n, k = kk, m = out_ch — and `col` is built
176                // row-major over kk, so it is transposed on the way in.
177                let mut colt = vec![0f32; col_len];
178                for r in 0..kk {
179                    for t in 0..out_n {
180                        colt[t * kk + r] = col[r * out_n + t];
181                    }
182                }
183                let mut yt = vec![0f32; out_n * self.out_ch];
184                if crate::gpu::gemm_nt_f32(&colt, &self.w, &mut yt, out_n, kk, self.out_ch) {
185                    for o in 0..self.out_ch {
186                        let bias = self.b.as_ref().map_or(0.0, |b| b[o]);
187                        // SAFETY: single-threaded here.
188                        let dst = unsafe { ptr.row(o * out_n, out_n) };
189                        for (t, d) in dst.iter_mut().enumerate() {
190                            *d = yt[t * self.out_ch + o] + bias;
191                        }
192                    }
193                    return out;
194                }
195            }
196        }
197        let tiles = (128 / self.out_ch.max(1)).max(1);
198        let tile = out_n.div_ceil(tiles.max(1));
199        let rows = self.out_ch * tiles;
200        let work = |lo: usize, hi: usize| {
201            for r in lo..hi {
202                let o = r / tiles;
203                let t0 = (r - o * tiles) * tile;
204                if t0 >= out_n {
205                    continue;
206                }
207                let len = tile.min(out_n - t0);
208                // SAFETY: workers own disjoint (channel, time) slices.
209                let dst = unsafe { ptr.row(o * out_n + t0, len) };
210                let bias = self.b.as_ref().map_or(0.0, |b| b[o]);
211                dst.fill(bias);
212                for i in 0..self.in_ch {
213                    let ker = &self.w[(o * self.in_ch + i) * self.k..(o * self.in_ch + i + 1) * self.k];
214                    let src = &x[i * n..(i + 1) * n];
215                    for (tt, d) in dst.iter_mut().enumerate() {
216                        let t = t0 + tt;
217                        let mut acc = 0f32;
218                        for (j, &kv) in ker.iter().enumerate() {
219                            let p = (t + j * self.dilation) as isize - self.pad as isize;
220                            if p >= 0 && (p as usize) < n {
221                                acc += kv * src[p as usize];
222                            }
223                        }
224                        *d += acc;
225                    }
226                }
227            }
228        };
229        match pool {
230            Some(p) => p.run_rows(rows, &work),
231            None => work(0, rows),
232        }
233        out
234    }
235}
236
237struct ConvT1d {
238    w: Vec<f32>, // [in, out, k]
239    b: Vec<f32>,
240    in_ch: usize,
241    out_ch: usize,
242    k: usize,
243    stride: usize,
244    pad: usize,
245}
246
247impl ConvT1d {
248    fn load(model: &Arc<CmfModel>, name: &str, stride: usize) -> Result<Self, String> {
249        let e = model
250            .tensor(&format!("{name}.weight"))
251            .ok_or_else(|| format!("missing {name}.weight"))?;
252        let (in_ch, out_ch, k) = (e.shape[0], e.shape[1], e.shape[2]);
253        Ok(Self {
254            w: crate::dit::cmf_f32(model, &format!("{name}.weight"))?,
255            b: crate::dit::cmf_f32(model, &format!("{name}.bias"))?,
256            in_ch,
257            out_ch,
258            k,
259            stride,
260            pad: (k - stride) / 2,
261        })
262    }
263
264    fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
265        let full = (n - 1) * self.stride + self.k;
266        let out_n = full - 2 * self.pad;
267        let mut out = vec![0f32; self.out_ch * out_n];
268        let ptr = SendPtr(out.as_mut_ptr());
269        let work = |lo: usize, hi: usize| {
270            for o in lo..hi {
271                // SAFETY: workers own disjoint output channels.
272                let dst = unsafe { ptr.row(o * out_n, out_n) };
273                dst.fill(self.b[o]);
274                for i in 0..self.in_ch {
275                    let ker = &self.w[(i * self.out_ch + o) * self.k..(i * self.out_ch + o + 1) * self.k];
276                    let src = &x[i * n..(i + 1) * n];
277                    for (t, &sv) in src.iter().enumerate() {
278                        if sv == 0.0 {
279                            continue;
280                        }
281                        let base = t * self.stride;
282                        for (j, &kv) in ker.iter().enumerate() {
283                            let p = base + j;
284                            if p >= self.pad && p - self.pad < out_n {
285                                dst[p - self.pad] += sv * kv;
286                            }
287                        }
288                    }
289                }
290            }
291        };
292        match pool {
293            Some(p) => p.run_rows(self.out_ch, &work),
294            None => work(0, self.out_ch),
295        }
296        out
297    }
298}
299
300/// Plain Snake, `x + sin²(α·x)/α`, with α used EXACTLY as stored.
301///
302/// The distinction from `SnakeBeta` below is not cosmetic: H3's BigVGAN
303/// keeps α and β in log scale and this file exponentiates them on load,
304/// while MiniMax-Music-3's decoder reads its α straight
305/// (`(alpha + 1e-9).reciprocal() * sin(alpha * x)**2`). Feeding one
306/// model's parameters to the other's loader silently raises the
307/// activation to an exponent, and the only symptom is worse audio.
308struct Snake {
309    alpha: Vec<f32>,
310}
311
312impl Snake {
313    fn load(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
314        Ok(Self {
315            alpha: crate::dit::cmf_f32(model, &format!("{name}.alpha"))?,
316        })
317    }
318
319    fn apply(&self, x: &mut [f32], n: usize) {
320        for (c, row) in x.chunks_exact_mut(n).enumerate() {
321            let a = self.alpha[c];
322            let inv = 1.0 / (a + 1e-9);
323            for v in row.iter_mut() {
324                let s = (a * *v).sin();
325                *v += s * s * inv;
326            }
327        }
328    }
329}
330
331/// One residual unit: Snake → dilated 7-tap → Snake → 1-tap, added back.
332struct DavUnit {
333    a1: Snake,
334    c1: Conv1d,
335    a2: Snake,
336    c2: Conv1d,
337}
338
339impl DavUnit {
340    fn load(model: &Arc<CmfModel>, p: &str, dilation: usize) -> Result<Self, String> {
341        Ok(Self {
342            a1: Snake::load(model, &format!("{p}.block.0"))?,
343            c1: Conv1d::load(model, &format!("{p}.block.1"), 3 * dilation, dilation)?,
344            a2: Snake::load(model, &format!("{p}.block.2"))?,
345            c2: Conv1d::load(model, &format!("{p}.block.3"), 0, 1)?,
346        })
347    }
348
349    fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
350        let mut h = x.to_vec();
351        self.a1.apply(&mut h, n);
352        let mut h = self.c1.apply(&h, n, pool);
353        self.a2.apply(&mut h, n);
354        let r = self.c2.apply(&h, n, pool);
355        // Same padding throughout, so the residual lines up without the
356        // reference's centre-crop; assert rather than trust that.
357        debug_assert_eq!(r.len(), x.len());
358        x.iter().zip(&r).map(|(a, b)| a + b).collect()
359    }
360}
361
362/// Snake → transposed conv (×stride) → three residual units at
363/// dilations 1, 3, 9.
364struct DavStage {
365    act: Snake,
366    up: ConvT1d,
367    units: Vec<DavUnit>,
368}
369
370impl DavStage {
371    fn load(model: &Arc<CmfModel>, p: &str, stride: usize) -> Result<Self, String> {
372        Ok(Self {
373            act: Snake::load(model, &format!("{p}.block.0"))?,
374            up: ConvT1d::load(model, &format!("{p}.block.1"), stride)?,
375            units: [1usize, 3, 9]
376                .iter()
377                .enumerate()
378                .map(|(i, &d)| DavUnit::load(model, &format!("{p}.block.{}", i + 2), d))
379                .collect::<Result<_, _>>()?,
380        })
381    }
382
383    fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> (Vec<f32>, usize) {
384        let mut h = x.to_vec();
385        self.act.apply(&mut h, n);
386        let mut h = self.up.apply(&h, n, pool);
387        let n = n * self.up.stride;
388        for u in &self.units {
389            h = u.apply(&h, n, pool);
390        }
391        (h, n)
392    }
393}
394
395/// MiniMax-Music-3's DAV decoder: latent → 44.1 kHz stereo.
396///
397/// The 128 latent channels are a STEREO PAIR of 64: the reference folds
398/// `[b, 128, t]` to `[b·2, 64, t]`, decodes mono, and unfolds. Reading
399/// 128 as one wide latent — which the vocoder config's `latent_channels`
400/// invites — decodes noise at half the length.
401pub struct Music3Dav {
402    dec_in: Conv1d,
403    conv_pre: Conv1d,
404    stages: Vec<DavStage>,
405    act_post: Snake,
406    conv_post: Conv1d,
407}
408
409impl Music3Dav {
410    pub const STRIDES: [usize; 4] = [8, 8, 4, 2];
411    /// Audio samples per latent frame: 8·8·4·2.
412    pub const HOP: usize = 512;
413    pub const SAMPLE_RATE: usize = 44100;
414
415    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
416        Ok(Self {
417            dec_in: Conv1d::load(model, "mvae.dec_in_proj", 0, 1)?,
418            conv_pre: Conv1d::load(model, "mvae.decoder.model.0", 3, 1)?,
419            stages: Self::STRIDES
420                .iter()
421                .enumerate()
422                .map(|(i, &s)| {
423                    DavStage::load(model, &format!("mvae.decoder.model.{}", i + 1), s)
424                })
425                .collect::<Result<_, _>>()?,
426            act_post: Snake::load(model, "mvae.decoder.model.5")?,
427            conv_post: Conv1d::load(model, "mvae.decoder.model.6", 3, 1)?,
428        })
429    }
430
431    /// `latent` is `[128, frames]`; the result is interleaved stereo of
432    /// `frames · 512` samples per channel.
433    pub fn decode(&self, latent: &[f32], frames: usize, pool: Option<&Pool>) -> Vec<f32> {
434        let mut chans: Vec<Vec<f32>> = Vec::with_capacity(2);
435        for half in 0..2 {
436            let src = &latent[half * 64 * frames..(half + 1) * 64 * frames];
437            let mut h = self.dec_in.apply(src, frames, pool);
438            h = self.conv_pre.apply(&h, frames, pool);
439            let mut n = frames;
440            for st in &self.stages {
441                let (nh, nn) = st.apply(&h, n, pool);
442                h = nh;
443                n = nn;
444            }
445            self.act_post.apply(&mut h, n);
446            let w = self.conv_post.apply(&h, n, pool);
447            // The reference ends in tanh; without it a loud latent
448            // clips as a wrap rather than a limit.
449            chans.push(w.iter().map(|v| v.tanh()).collect());
450        }
451        let n = chans[0].len();
452        let mut out = vec![0f32; n * 2];
453        for (i, o) in out.chunks_exact_mut(2).enumerate() {
454            o[0] = chans[0][i];
455            o[1] = chans[1][i];
456        }
457        out
458    }
459}
460
461/// `x + sin²(α·x)/β`, with α and β stored in log scale.
462struct SnakeBeta {
463    alpha: Vec<f32>,
464    beta: Vec<f32>,
465}
466
467impl SnakeBeta {
468    fn load(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
469        Ok(Self {
470            alpha: crate::dit::cmf_f32(model, &format!("{name}.alpha"))?
471                .iter()
472                .map(|v| v.exp())
473                .collect(),
474            beta: crate::dit::cmf_f32(model, &format!("{name}.beta"))?
475                .iter()
476                .map(|v| v.exp())
477                .collect(),
478        })
479    }
480
481    fn apply(&self, x: &mut [f32], n: usize) {
482        for (c, row) in x.chunks_exact_mut(n).enumerate() {
483            let (a, b) = (self.alpha[c], 1.0 / (self.beta[c] + 1e-9));
484            for v in row.iter_mut() {
485                let s = (a * *v).sin();
486                *v += s * s * b;
487            }
488        }
489    }
490}
491
492fn bessel_i0(x: f64) -> f64 {
493    // Series; the argument here is ~4.7, where a dozen terms is exact
494    // to double precision.
495    let mut sum = 1.0;
496    let mut term = 1.0;
497    for k in 1..40 {
498        term *= (x / (2.0 * k as f64)).powi(2);
499        sum += term;
500        if term < 1e-18 * sum {
501            break;
502        }
503    }
504    sum
505}
506
507fn sinc(x: f64) -> f64 {
508    if x == 0.0 {
509        1.0
510    } else {
511        (std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x)
512    }
513}
514
515/// The reference's `kaiser_sinc_filter1d`, normalized to unit sum.
516fn kaiser_sinc(cutoff: f64, half_width: f64, k: usize) -> Vec<f32> {
517    let half = k / 2;
518    let delta_f = 4.0 * half_width;
519    let a = 2.285 * (half as f64 - 1.0) * std::f64::consts::PI * delta_f + 7.95;
520    let beta = if a > 50.0 {
521        0.1102 * (a - 8.7)
522    } else if a >= 21.0 {
523        0.5842 * (a - 21.0).powf(0.4) + 0.078_86 * (a - 21.0)
524    } else {
525        0.0
526    };
527    let denom = bessel_i0(beta);
528    let n = k as f64 - 1.0;
529    let mut f: Vec<f64> = (0..k)
530        .map(|i| {
531            let r = (2.0 * i as f64 / n) - 1.0;
532            let win = bessel_i0(beta * (1.0 - r * r).max(0.0).sqrt()) / denom;
533            // even length: sample points sit on half-integers
534            let t = -(half as f64) + i as f64 + 0.5;
535            2.0 * cutoff * win * sinc(2.0 * cutoff * t)
536        })
537        .collect();
538    let s: f64 = f.iter().sum();
539    for v in f.iter_mut() {
540        *v /= s;
541    }
542    f.into_iter().map(|v| v as f32).collect()
543}
544
545/// Replicate-pad, then a per-channel FIR.
546///
547/// MEASURED, and smaller than it looked: parallelizing this took the
548/// audio stage 9.2 s → 8.1 s, output bit-identical. So the FIR is not
549/// where that stage's time goes — the rest is in the convolutions,
550/// which DO use the pool but split over OUTPUT CHANNELS, and this
551/// decoder narrows to a handful of them near the output where the
552/// samples are longest. Splitting those over TIME instead is the next
553/// thing to try, and it wants a phase profiler first: this decoder
554/// still has none.
555///
556/// Original note: the audio decoder is 9.2 s of a 95.6 s render — the same share the video decoder had
557/// before its host loops met the thread pool (38.3 s → 16.6 s). This
558/// function is the shape that fix wanted: every channel is independent
559/// and the whole thing runs on one thread. The convolutions above DO
560/// use the pool, but they split over OUTPUT CHANNELS, and this decoder
561/// narrows to a handful of them near the output — where the samples
562/// are longest and the parallelism collapses exactly when it is needed.
563///
564/// It needs a `pool` argument threaded from `Activation1d`/`AudioVae`
565/// and a per-worker `buf` instead of the shared one. Measure first with
566/// a phase profiler like `CMF_VAE3D_PROF`: this decoder has none, and
567/// the video one went untuned for a whole session precisely because
568/// nothing measured it.
569#[allow(clippy::too_many_arguments)]
570fn fir_pad(
571    x: &[f32],
572    ch: usize,
573    n: usize,
574    f: &[f32],
575    pad_l: usize,
576    pad_r: usize,
577    stride: usize,
578    pool: Option<&Pool>,
579) -> (Vec<f32>, usize) {
580    let padded = n + pad_l + pad_r;
581    let out_n = (padded - f.len()) / stride + 1;
582    let mut out = vec![0f32; ch * out_n];
583    struct P(*mut f32);
584    // SAFETY: each channel owns `out[c*out_n .. (c+1)*out_n]` and no
585    // two workers take the same channel.
586    unsafe impl Send for P {}
587    unsafe impl Sync for P {}
588    impl P {
589        // Through a method, so the closure captures the WRAPPER and not
590        // the bare pointer — 2021 captures disjoint fields, and a
591        // captured `*mut f32` is neither Send nor Sync.
592        #[allow(clippy::mut_from_ref)]
593        unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
594            unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
595        }
596    }
597    let po = P(out.as_mut_ptr());
598    let work = |lo: usize, hi: usize| {
599        // Per worker, not shared: the old single `buf` is what kept this
600        // on one thread.
601        let mut buf = vec![0f32; padded];
602        for c in lo..hi {
603            let src = &x[c * n..(c + 1) * n];
604            for (i, b) in buf.iter_mut().enumerate() {
605                let p = i as isize - pad_l as isize;
606                *b = src[p.clamp(0, n as isize - 1) as usize];
607            }
608            let dst = unsafe { po.row(c * out_n, out_n) };
609            for (t, d) in dst.iter_mut().enumerate() {
610                let mut acc = 0f32;
611                for (j, &kv) in f.iter().enumerate() {
612                    acc += kv * buf[t * stride + j];
613                }
614                *d = acc;
615            }
616        }
617    };
618    match pool {
619        Some(p) => p.run_rows(ch, &work),
620        None => work(0, ch),
621    }
622    (out, out_n)
623}
624
625/// Upsample ×2, apply, downsample ×2 — the anti-aliased activation.
626struct Activation1d {
627    act: SnakeBeta,
628    up: Vec<f32>,
629    down: Vec<f32>,
630}
631
632impl Activation1d {
633    /// `name` is the Activation1d module, not its `.act`. The release
634    /// ships both resampling filters as buffers — 254 of them — so read
635    /// them rather than re-designing them, and keep `kaiser_sinc` as
636    /// the fallback for a checkpoint that drops them.
637    fn load(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
638        let designed = || kaiser_sinc(0.25, 0.3, FILTER_LEN);
639        Ok(Self {
640            act: SnakeBeta::load(model, &format!("{name}.act"))?,
641            up: crate::dit::cmf_f32(model, &format!("{name}.upsample.filter"))
642                .unwrap_or_else(|_| designed()),
643            down: crate::dit::cmf_f32(model, &format!("{name}.downsample.lowpass.filter"))
644                .unwrap_or_else(|_| designed()),
645        })
646    }
647
648    fn apply(&self, x: &[f32], ch: usize, n: usize, pool: Option<&Pool>) -> (Vec<f32>, usize) {
649        // conv_transpose1d(pad(x, 5, 5), filter, stride 2) · 2, then the
650        // 15-sample margins the reference trims off each end.
651        let pad = FILTER_LEN / 2 - 1;
652        let pad_l = pad * 2 + (FILTER_LEN - 2) / 2;
653        let pad_r = pad * 2 + (FILTER_LEN - 2 + 1) / 2;
654        let pn = n + 2 * pad;
655        let full = (pn - 1) * 2 + FILTER_LEN;
656        let mut up = vec![0f32; ch * full];
657        for c in 0..ch {
658            let src = &x[c * n..(c + 1) * n];
659            let dst = &mut up[c * full..(c + 1) * full];
660            for i in 0..pn {
661                let p = i as isize - pad as isize;
662                let v = src[p.clamp(0, n as isize - 1) as usize] * 2.0;
663                if v == 0.0 {
664                    continue;
665                }
666                for (j, &kv) in self.up.iter().enumerate() {
667                    dst[i * 2 + j] += v * kv;
668                }
669            }
670        }
671        let keep = full - pad_l - pad_r;
672        let mut mid = vec![0f32; ch * keep];
673        for c in 0..ch {
674            mid[c * keep..(c + 1) * keep]
675                .copy_from_slice(&up[c * full + pad_l..c * full + pad_l + keep]);
676        }
677        self.act.apply(&mut mid, keep);
678        // LowPassFilter1d at stride 2: even kernel pads 5 left, 6 right.
679        fir_pad(&mid, ch, keep, &self.down, FILTER_LEN / 2 - 1, FILTER_LEN / 2, 2, pool)
680    }
681}
682
683struct AmpBlock {
684    convs1: Vec<Conv1d>,
685    convs2: Vec<Conv1d>,
686    acts: Vec<Activation1d>,
687}
688
689pub struct AudioVae {
690    dec_in: Conv1d,
691    conv_pre: Conv1d,
692    ups: Vec<ConvT1d>,
693    resblocks: Vec<AmpBlock>,
694    act_post: Activation1d,
695    conv_post: Conv1d,
696    latents_mean: Vec<f32>,
697    latents_std: Vec<f32>,
698    pool: Option<Arc<Pool>>,
699    n_kernels: usize,
700    pub sample_rate: usize,
701}
702
703fn get_padding(k: usize, d: usize) -> usize {
704    (k * d - d) / 2
705}
706
707impl AudioVae {
708    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
709        let cfg: serde_json::Value = serde_json::from_slice(
710            model.tensor_bytes("avae.config_json").map_err(|e| e.to_string())?,
711        )
712        .map_err(|e| format!("avae.config_json: {e}"))?;
713        let rates: Vec<usize> = cfg["upsample_rates"]
714            .as_array()
715            .ok_or("upsample_rates")?
716            .iter()
717            .map(|v| v.as_u64().unwrap_or(1) as usize)
718            .collect();
719        let rk: Vec<usize> = cfg["resblock_kernel_sizes"]
720            .as_array()
721            .ok_or("resblock_kernel_sizes")?
722            .iter()
723            .map(|v| v.as_u64().unwrap_or(3) as usize)
724            .collect();
725        let rd: Vec<Vec<usize>> = cfg["resblock_dilation_sizes"]
726            .as_array()
727            .ok_or("resblock_dilation_sizes")?
728            .iter()
729            .map(|a| {
730                a.as_array()
731                    .unwrap()
732                    .iter()
733                    .map(|v| v.as_u64().unwrap_or(1) as usize)
734                    .collect()
735            })
736            .collect();
737
738        let mut ups = Vec::new();
739        for (i, &u) in rates.iter().enumerate() {
740            ups.push(ConvT1d::load(model, &format!("avae.decoder.ups.{i}.0"), u)?);
741        }
742        let mut resblocks = Vec::new();
743        for i in 0..rates.len() {
744            for (j, (&k, d)) in rk.iter().zip(&rd).enumerate() {
745                let p = format!("avae.decoder.resblocks.{}", i * rk.len() + j);
746                let convs1 = (0..d.len())
747                    .map(|q| Conv1d::load(model, &format!("{p}.convs1.{q}"), get_padding(k, d[q]), d[q]))
748                    .collect::<Result<Vec<_>, _>>()?;
749                let convs2 = (0..d.len())
750                    .map(|q| Conv1d::load(model, &format!("{p}.convs2.{q}"), get_padding(k, 1), 1))
751                    .collect::<Result<Vec<_>, _>>()?;
752                let acts = (0..convs1.len() + convs2.len())
753                    .map(|q| Activation1d::load(model, &format!("{p}.activations.{q}")))
754                    .collect::<Result<Vec<_>, _>>()?;
755                resblocks.push(AmpBlock { convs1, convs2, acts });
756            }
757        }
758        Ok(Self {
759            dec_in: Conv1d::load(model, "avae.dec_in_proj", 0, 1)?,
760            conv_pre: Conv1d::load(model, "avae.decoder.conv_pre", 3, 1)?,
761            ups,
762            resblocks,
763            act_post: Activation1d::load(model, "avae.decoder.activation_post")?,
764            conv_post: Conv1d::load(model, "avae.decoder.conv_post", 3, 1)?,
765            latents_mean: crate::dit::cmf_f32(model, "avae.latents_mean")?,
766            latents_std: crate::dit::cmf_f32(model, "avae.latents_std")?,
767            pool: Pool::from_env(),
768            n_kernels: rk.len(),
769            sample_rate: cfg["sample_rate"].as_u64().unwrap_or(32000) as usize,
770        })
771    }
772
773    /// Normalized latents `[C, 2, T]` → stereo `[2, L]` in [-1, 1].
774    ///
775    /// THE REMAINING WIN IS THIS LOOP: the two stereo channels are two
776    /// complete, independent decoder passes and they run one after the
777    /// other. That is a factor of two sitting outside the exact place
778    /// the inside cannot use — the convolutions parallelize over output
779    /// channels, and this decoder narrows to a handful of those near the
780    /// output where the samples are longest (measured: parallelizing the
781    /// per-channel FIR bought only 9.2 s → 8.1 s, so the time is in the
782    /// convolutions, not the filter).
783    ///
784    /// The nesting question is ANSWERED, and the answer forbids the
785    /// obvious version: `Pool` keeps ONE job slot (`inner.slot`), and
786    /// `run()` writes it on the stated assumption that no job is in
787    /// flight. Two concurrent callers would overwrite each other's job.
788    /// So wrapping this loop in a `thread::scope` while the passes still
789    /// call into the pool is a data race, not an optimization.
790    ///
791    /// Two shapes remain. Drive the pool from the OUTSIDE — one row per
792    /// stereo channel, `None` inside — which is correct but caps the
793    /// whole decode at two threads and would lose the wide layers what
794    /// it wins the narrow ones. Or split the narrow convolutions over
795    /// TIME instead of output channels, which keeps every thread busy
796    /// at both ends. Measure before choosing: this decoder still has no
797    /// phase profiler, and the FIR already proved the shape of the code
798    /// is a poor guide to where its seconds are.
799    pub fn decode(&self, z: &[f32], c: usize, t: usize) -> (Vec<f32>, usize) {
800        let pool = self.pool.as_deref();
801        let mut chans: Vec<Vec<f32>> = Vec::with_capacity(2);
802        for ch in 0..2 {
803            let mut lat = vec![0f32; c * t];
804            for ci in 0..c {
805                let (m, s) = (self.latents_mean[ci], self.latents_std[ci]);
806                for ti in 0..t {
807                    lat[ci * t + ti] = z[(ci * 2 + ch) * t + ti] * s + m;
808                }
809            }
810            // `CMF_AVAE_PROF=1`: per-stage rms, to diff against the
811            // reference stage by stage rather than at the waveform.
812            let prof = std::env::var_os("CMF_AVAE_PROF").is_some();
813            let rms = |x: &[f32]| (x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>()
814                / x.len() as f64)
815                .sqrt();
816            let tt = std::time::Instant::now();
817            let mut x = self.dec_in.apply(&lat, t, pool);
818            atime(0, tt);
819            let mut n = t;
820            if prof {
821                eprintln!("ch{ch} dec_in rms {:.6e} n {n}", rms(&x));
822            }
823            let tt = std::time::Instant::now();
824            x = self.conv_pre.apply(&x, n, pool);
825            atime(1, tt);
826            if prof {
827                eprintln!("ch{ch} conv_pre rms {:.6e} n {n}", rms(&x));
828            }
829            for i in 0..self.ups.len() {
830                let up = &self.ups[i];
831                let tt = std::time::Instant::now();
832                x = up.apply(&x, n, pool);
833                atime(2, tt);
834                n = (n - 1) * up.stride + up.k - 2 * up.pad;
835                let ch_n = up.out_ch;
836                let mut acc = vec![0f32; ch_n * n];
837                for j in 0..self.n_kernels {
838                    let tt = std::time::Instant::now();
839                    let r = self.resblocks[i * self.n_kernels + j].apply(&x, ch_n, n, pool);
840                    atime(3, tt);
841                    for (a, b) in acc.iter_mut().zip(&r) {
842                        *a += b;
843                    }
844                }
845                let inv = 1.0 / self.n_kernels as f32;
846                for v in acc.iter_mut() {
847                    *v *= inv;
848                }
849                x = acc;
850                if prof {
851                    eprintln!("ch{ch} up{i} rms {:.6e} ch {ch_n} n {n}", rms(&x));
852                }
853            }
854            let last_ch = self.ups[self.ups.len() - 1].out_ch;
855            let tt = std::time::Instant::now();
856            let (mut y, yn) = self.act_post.apply(&x, last_ch, n, pool);
857            atime(4, tt);
858            let tt = std::time::Instant::now();
859            y = self.conv_post.apply(&y, yn, pool);
860            atime(5, tt);
861            for v in y.iter_mut() {
862                *v = v.clamp(-1.0, 1.0);
863            }
864            chans.push(y);
865            n = yn;
866            let _ = n;
867        }
868        let len = chans[0].len().min(chans[1].len());
869        let mut out = vec![0f32; 2 * len];
870        for (ch, c) in chans.iter().enumerate() {
871            out[ch * len..(ch + 1) * len].copy_from_slice(&c[..len]);
872        }
873        (out, len)
874    }
875}
876
877impl AmpBlock {
878    fn apply(&self, x: &[f32], ch: usize, n: usize, pool: Option<&Pool>) -> Vec<f32> {
879        let mut cur = x.to_vec();
880        for i in 0..self.convs1.len() {
881            let (a1, a2) = (&self.acts[i * 2], &self.acts[i * 2 + 1]);
882            let (xt, tn) = a1.apply(&cur, ch, n, pool);
883            let xt = self.convs1[i].apply(&xt, tn, pool);
884            let (xt, tn2) = a2.apply(&xt, ch, tn, pool);
885            let xt = self.convs2[i].apply(&xt, tn2, pool);
886            for (a, b) in cur.iter_mut().zip(&xt) {
887                *a += b;
888            }
889        }
890        cur
891    }
892}
893
894/// Test hook: the designed 12-tap resampling filter.
895#[doc(hidden)]
896pub fn kaiser_sinc_for_test() -> Vec<f32> {
897    kaiser_sinc(0.25, 0.3, FILTER_LEN)
898}
899
900struct SendPtr(*mut f32);
901unsafe impl Send for SendPtr {}
902unsafe impl Sync for SendPtr {}
903impl SendPtr {
904    /// SAFETY: caller guarantees disjoint `[off, off+len)` per worker.
905    #[allow(clippy::mut_from_ref)]
906    unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
907        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
908    }
909}
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914
915    /// Decode through the packed MiniMax-Music-3 vocoder and check the
916    /// two things the reference fixes exactly: 512 samples per latent
917    /// frame per side, and a `tanh` range. `CMF_MUSIC3_VAE=<file.cmf>`
918    /// points at a pack; without it there is nothing to test against.
919    #[test]
920    fn music3_dav_decodes_to_the_reference_geometry() {
921        let Ok(p) = std::env::var("CMF_MUSIC3_VAE") else {
922            eprintln!("CMF_MUSIC3_VAE unset — skipping Music-3 vocoder test");
923            return;
924        };
925        let model = Arc::new(CmfModel::open(&p).expect("open packed vocoder"));
926        let dav = Music3Dav::from_cmf(&model).expect("load DAV");
927        let frames = 12usize;
928        // A latent with structure rather than noise: a decoder that has
929        // silently lost a stage still returns *something* for noise.
930        let latent: Vec<f32> = (0..128 * frames)
931            .map(|i| {
932                let (c, t) = (i / frames, i % frames);
933                0.4 * ((c as f32 * 0.13 + t as f32 * 0.7).sin())
934            })
935            .collect();
936        let pcm = dav.decode(&latent, frames, None);
937        assert_eq!(
938            pcm.len(),
939            frames * Music3Dav::HOP * 2,
940            "512 samples a frame, two sides interleaved"
941        );
942        assert!(pcm.iter().all(|v| v.is_finite()), "non-finite sample");
943        assert!(
944            pcm.iter().all(|v| v.abs() <= 1.0),
945            "tanh range violated: {}",
946            pcm.iter().fold(0f32, |m, v| m.max(v.abs()))
947        );
948        // Silence would also satisfy the above; a working stack moves.
949        let rms = (pcm.iter().map(|v| v * v).sum::<f32>() / pcm.len() as f32).sqrt();
950        assert!(rms > 1e-4, "decoded to near-silence, rms {rms}");
951        let (l, r): (Vec<f32>, Vec<f32>) =
952            pcm.chunks_exact(2).map(|c| (c[0], c[1])).unzip();
953        let d = l.iter().zip(&r).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
954        assert!(d > 0.0, "both sides identical — the 128 latent was not split");
955        eprintln!("music3 dav: {} samples/side, rms {rms:.4}, L-R max {d:.4}", l.len());
956    }
957
958    #[test]
959    fn the_resampling_filter_is_the_references() {
960        let f = kaiser_sinc(0.25, 0.3, FILTER_LEN);
961        assert_eq!(f.len(), FILTER_LEN);
962        // Unit sum: without it a constant input leaks amplitude, which
963        // is the whole reason the reference normalizes.
964        assert!((f.iter().sum::<f32>() - 1.0).abs() < 1e-6);
965        // Symmetric about the centre, and its peak is at the centre.
966        for i in 0..FILTER_LEN / 2 {
967            assert!((f[i] - f[FILTER_LEN - 1 - i]).abs() < 1e-6, "asymmetric at {i}");
968        }
969        let peak = f.iter().cloned().fold(f32::MIN, f32::max);
970        assert!((f[5] - peak).abs() < 1e-6);
971
972    }
973
974    #[test]
975    fn bessel_i0_matches_known_values() {
976        // The third is the β the 12-tap filter's Kaiser window is
977        // designed at, so it is the value that actually gets used.
978        for (x, want) in [(0.0, 1.0), (1.0, 1.266_065_878), (4.664, 20.204_6)] {
979            let got = bessel_i0(x);
980            assert!((got - want).abs() < 1e-3 * want.max(1.0), "I0({x}) = {got}");
981        }
982    }
983}