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