1use crate::egemma2::{EmbeddingGemma2, Mat, TextInput, rms_into, rms_rows, vecf};
51use crate::ltxdit::{Shared, rows};
52use crate::pool::Pool;
53use cortiq_core::CmfModel;
54use std::sync::{Arc, Mutex};
55
56pub const SAMPLE_RATE: u32 = 16_000;
58pub const MAX_SAMPLES: usize = 480_000;
60pub const FRAME: usize = 320;
62pub const HOP: usize = 160;
63pub const N_FFT: usize = 512;
65pub const N_MELS: usize = 128;
67pub const MEL_FLOOR: f64 = 1e-3;
69pub const PLACEHOLDER: &str = "<|audio|>";
71pub const AUDIO_TOKEN: u32 = 258_881;
74pub const BOA_TOKEN: u32 = 256_000;
75pub const EOA_TOKEN: u32 = 258_883;
76
77const EPS: f64 = 1e-6;
78const PACK_SOFT: usize = 4096;
80
81mod prof {
84 use std::sync::atomic::{AtomicU64, Ordering};
85 pub static MEL: AtomicU64 = AtomicU64::new(0);
86 pub static SUB: AtomicU64 = AtomicU64::new(0);
87 pub static LIN: AtomicU64 = AtomicU64::new(0);
88 pub static ATT: AtomicU64 = AtomicU64::new(0);
89 pub fn on() -> bool {
90 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
91 *ON.get_or_init(|| std::env::var("CMF_EGEMMA2_PROF").is_ok_and(|v| v == "1"))
92 }
93 pub fn add(c: &AtomicU64, t: std::time::Instant) {
94 c.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
95 }
96 pub fn take(c: &AtomicU64) -> f64 {
97 c.swap(0, Ordering::Relaxed) as f64 / 1e3
98 }
99}
100
101pub fn num_frames(n: usize) -> usize {
105 let n = n.min(MAX_SAMPLES);
106 if n <= HOP { 0 } else { (n - HOP).div_ceil(HOP) }
107}
108
109pub fn num_tokens(n: usize) -> usize {
112 num_frames(n).div_ceil(2).div_ceil(2)
113}
114
115pub fn decode_audio(bytes: &[u8]) -> Result<Vec<f32>, String> {
123 let wav = if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WAVE" {
124 crate::mimo_audio::decode_wav(bytes)?
125 } else {
126 crate::mimo_audio::decode_wav(&ffmpeg_to_wav(bytes)?)?
127 };
128 Ok(to_mono_16k(&wav))
129}
130
131pub fn read_audio(path: &std::path::Path) -> Result<Vec<f32>, String> {
133 let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
134 decode_audio(&bytes).map_err(|e| format!("{}: {e}", path.display()))
135}
136
137pub fn load_audio_source(v: &serde_json::Value) -> Result<Vec<f32>, String> {
142 use serde_json::{Value, json};
143 let record = match v {
144 Value::String(s) => json!({ "url": s }),
145 Value::Object(m) => {
146 if let Some(inner) = m.get("input_audio") {
147 return load_audio_source(inner);
148 }
149 if let Some(p) = m.get("path").and_then(Value::as_str) {
150 json!({ "url": p })
151 } else {
152 v.clone()
153 }
154 }
155 _ => return Err("an audio source must be a string or an object".into()),
156 };
157 let bytes = crate::media::load_image_bytes(&record).map_err(|e| e.replace("image", "audio"))?;
158 decode_audio(&bytes)
159}
160
161pub fn to_mono_16k(wav: &crate::mimo_audio::Wav) -> Vec<f32> {
163 let n = wav.frames();
164 let mono: Vec<f32> = match wav.channels.len() {
165 0 => Vec::new(),
166 1 => wav.channels[0].clone(),
167 c => (0..n)
168 .map(|i| wav.channels.iter().map(|ch| ch[i]).sum::<f32>() / c as f32)
169 .collect(),
170 };
171 resample_hq(&mono, wav.sample_rate, SAMPLE_RATE)
172}
173
174const RS_WIDTH: f64 = 64.0;
177const RS_ROLLOFF: f64 = 0.958;
178const RS_BETA: f64 = 12.8;
179const RS_TABLE_MAX: usize = 1 << 22;
181
182fn bessel_i0(x: f64) -> f64 {
184 let q = x * x / 4.0;
185 let (mut term, mut sum) = (1.0f64, 1.0f64);
186 for k in 1..200 {
187 term *= q / (k * k) as f64;
188 sum += term;
189 if term < sum * 1e-17 {
190 break;
191 }
192 }
193 sum
194}
195
196pub fn resample_hq(x: &[f32], from: u32, to: u32) -> Vec<f32> {
207 if from == to || x.is_empty() {
208 return x.to_vec();
209 }
210 let g = {
211 let (mut a, mut b) = (from as u64, to as u64);
212 while b != 0 {
213 (a, b) = (b, a % b);
214 }
215 a
216 };
217 let (o, nw) = ((from as u64 / g) as usize, (to as u64 / g) as usize);
218 let len = x.len();
219 let target = ((nw as u128 * len as u128).div_ceil(o as u128)) as usize;
220 let base = o.min(nw) as f64 * RS_ROLLOFF;
221 let w = (RS_WIDTH * o as f64 / base).ceil() as usize;
222 let taps = 2 * w + o;
223 let i0b = bessel_i0(RS_BETA);
224 let scale = base / o as f64;
225 let coef = |j: usize, i: usize| -> f64 {
226 let t = (-(j as f64) / nw as f64 + (i as f64 - w as f64) / o as f64) * base;
227 if t.abs() >= RS_WIDTH {
228 return 0.0;
229 }
230 let r = t / RS_WIDTH;
231 let win = bessel_i0(RS_BETA * (1.0 - r * r).sqrt()) / i0b;
232 let pt = t * std::f64::consts::PI;
233 let sinc = if pt == 0.0 { 1.0 } else { pt.sin() / pt };
234 sinc * win * scale
235 };
236 let table = nw * taps <= RS_TABLE_MAX;
238 let span = |j: usize| -> (usize, usize) {
239 let c = w as f64 + o as f64 * j as f64 / nw as f64;
240 let h = RS_WIDTH * o as f64 / base;
241 let lo = (c - h).floor().max(0.0) as usize;
242 let hi = ((c + h).ceil() as usize + 1).min(taps);
243 (lo, hi)
244 };
245 let rows: Vec<(usize, Vec<f64>)> = if table {
246 (0..nw)
247 .map(|j| {
248 let (lo, hi) = span(j);
249 (lo, (lo..hi).map(|i| coef(j, i)).collect())
250 })
251 .collect()
252 } else {
253 Vec::new()
254 };
255 let mut out = vec![0f32; target];
256 let threads = std::thread::available_parallelism()
257 .map(|n| n.get())
258 .unwrap_or(1)
259 .clamp(1, 8);
260 let chunk = target.div_ceil(threads).max(4096);
261 std::thread::scope(|sc| {
262 for (ci, dst) in out.chunks_mut(chunk).enumerate() {
263 let (rows, coef, span) = (&rows, &coef, &span);
264 sc.spawn(move || {
265 let mut tmp = Vec::new();
266 for (k, y) in dst.iter_mut().enumerate() {
267 let n_out = ci * chunk + k;
268 let (f, j) = (n_out / nw, n_out % nw);
269 let (lo, ks): (usize, &[f64]) = if table {
270 (rows[j].0, &rows[j].1)
271 } else {
272 let (lo, hi) = span(j);
273 tmp.clear();
274 tmp.extend((lo..hi).map(|i| coef(j, i)));
275 (lo, &tmp)
276 };
277 let p0 = (f * o + lo) as isize - w as isize;
279 let mut acc = 0f64;
280 for (m, &kv) in ks.iter().enumerate() {
281 let p = p0 + m as isize;
282 if p >= 0 && (p as usize) < len {
283 acc += kv * x[p as usize] as f64;
284 }
285 }
286 *y = acc as f32;
287 }
288 });
289 }
290 });
291 out
292}
293
294fn ffmpeg_to_wav(bytes: &[u8]) -> Result<Vec<u8>, String> {
297 let dir = std::env::temp_dir();
299 let src = dir.join(format!(
300 "cortiq-egemma2-audio-{}-{}.in",
301 std::process::id(),
302 std::time::SystemTime::now()
303 .duration_since(std::time::UNIX_EPOCH)
304 .map(|d| d.as_nanos())
305 .unwrap_or(0)
306 ));
307 std::fs::write(&src, bytes).map_err(|e| format!("audio: temp file: {e}"))?;
308 let out = std::process::Command::new("ffmpeg")
309 .args(["-nostdin", "-v", "error", "-i"])
310 .arg(&src)
311 .args(["-vn", "-f", "wav", "-acodec", "pcm_f32le", "pipe:1"])
312 .output();
313 let _ = std::fs::remove_file(&src);
314 match out {
315 Err(_) => Err(
316 "audio: not a WAV file, and ffmpeg (needed for other formats) is not installed".into(),
317 ),
318 Ok(o) if !o.status.success() || o.stdout.len() < 44 => {
319 let err = String::from_utf8_lossy(&o.stderr);
321 let why = err
322 .lines()
323 .rev()
324 .find(|l| !l.trim().is_empty())
325 .unwrap_or("")
326 .trim();
327 Err(format!(
328 "audio: not a WAV file, and ffmpeg could not decode it either ({why})"
329 ))
330 }
331 Ok(o) => Ok(o.stdout),
332 }
333}
334
335fn linspace(a: f64, b: f64, n: usize) -> Vec<f64> {
339 let step = (b - a) / (n - 1) as f64;
340 let mut v: Vec<f64> = (0..n).map(|i| a + i as f64 * step).collect();
341 v[n - 1] = b;
342 v
343}
344
345pub fn mel_filter_bank() -> Vec<f64> {
348 let n_freq = N_FFT / 2 + 1;
349 let hz2mel = |f: f64| 2595.0 * (1.0 + f / 700.0).log10();
350 let mel2hz = |m: f64| 700.0 * (10f64.powf(m / 2595.0) - 1.0);
351 let mels = linspace(hz2mel(0.0), hz2mel(8000.0), N_MELS + 2);
352 let ff: Vec<f64> = mels.iter().map(|&m| mel2hz(m)).collect();
353 let fft = linspace(0.0, (SAMPLE_RATE / 2) as f64, n_freq);
354 let mut out = vec![0f64; n_freq * N_MELS];
355 for (k, &f) in fft.iter().enumerate() {
356 for m in 0..N_MELS {
357 let down = -(ff[m] - f) / (ff[m + 1] - ff[m]);
358 let up = (ff[m + 2] - f) / (ff[m + 2] - ff[m + 1]);
359 out[k * N_MELS + m] = 0f64.max(down.min(up));
360 }
361 }
362 out
363}
364
365pub struct MelFrontend {
367 window: Vec<f32>,
368 filters: Vec<f64>,
370 fft: Arc<dyn rustfft::Fft<f64>>,
371}
372
373impl Default for MelFrontend {
374 fn default() -> Self {
375 Self::new()
376 }
377}
378
379impl MelFrontend {
380 pub fn new() -> Self {
381 let window = (0..FRAME)
383 .map(|n| {
384 (0.5 - 0.5 * (2.0 * std::f64::consts::PI * n as f64 / FRAME as f64).cos()) as f32
385 })
386 .collect();
387 let fft = rustfft::FftPlanner::<f64>::new().plan_fft_forward(N_FFT);
388 MelFrontend {
389 window,
390 filters: mel_filter_bank(),
391 fft,
392 }
393 }
394
395 pub fn log_mel(&self, wave: &[f32], pool: Option<&Pool>) -> (Vec<f32>, usize) {
400 let wave = &wave[..wave.len().min(MAX_SAMPLES)];
401 let n = num_frames(wave.len());
402 let mut out = vec![0f32; n * N_MELS];
403 let dst = Shared(out.as_mut_ptr());
404 let n_freq = N_FFT / 2 + 1;
405 rows(pool, n, &|s, e| {
406 let o = unsafe { dst.at(s * N_MELS, (e - s) * N_MELS) };
407 let mut buf = vec![rustfft::num_complex::Complex::<f64>::default(); N_FFT];
408 let mut scratch = vec![
409 rustfft::num_complex::Complex::<f64>::default();
410 self.fft.get_inplace_scratch_len()
411 ];
412 let mut mag = vec![0f64; n_freq];
413 for (i, row) in (s..e).zip(o.chunks_exact_mut(N_MELS)) {
414 for (j, b) in buf.iter_mut().enumerate() {
415 let v = if j < FRAME {
417 let p = i * HOP + j;
418 let x = if p >= HOP { wave[p - HOP] } else { 0.0 };
419 (x * self.window[j]) as f64
420 } else {
421 0.0
422 };
423 *b = rustfft::num_complex::Complex::new(v, 0.0);
424 }
425 self.fft.process_with_scratch(&mut buf, &mut scratch);
426 for (m, c) in mag.iter_mut().zip(&buf[..n_freq]) {
427 *m = (c.re.hypot(c.im) as f32) as f64;
428 }
429 for (b, r) in row.iter_mut().enumerate() {
430 let mut acc = 0f64;
431 for (k, &m) in mag.iter().enumerate() {
432 acc += m * self.filters[k * N_MELS + b];
433 }
434 *r = (acc + MEL_FLOOR).ln() as f32;
435 }
436 }
437 });
438 (out, n)
439 }
440}
441
442struct ClipLin {
447 w: Mat,
448 lo_in: f32,
449 hi_in: f32,
450 lo_out: f32,
451 hi_out: f32,
452}
453
454fn scalar(model: &CmfModel, name: &str, default: f32) -> Result<f32, String> {
455 if model.tensor(name).is_none() {
456 return Ok(default);
457 }
458 Ok(vecf(model, name)?.first().copied().unwrap_or(default))
459}
460
461impl ClipLin {
462 fn load(model: &Arc<CmfModel>, p: &str, dequant: bool) -> Result<Self, String> {
463 Ok(ClipLin {
464 w: Mat::load(model, &format!("{p}.linear.weight"), dequant)?,
465 lo_in: scalar(model, &format!("{p}.input_min"), f32::NEG_INFINITY)?,
466 hi_in: scalar(model, &format!("{p}.input_max"), f32::INFINITY)?,
467 lo_out: scalar(model, &format!("{p}.output_min"), f32::NEG_INFINITY)?,
468 hi_out: scalar(model, &format!("{p}.output_max"), f32::INFINITY)?,
469 })
470 }
471
472 fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
473 let mut xc: Vec<f32>;
474 let x = if self.lo_in.is_finite() || self.hi_in.is_finite() {
475 xc = x.to_vec();
476 let (lo, hi) = (self.lo_in, self.hi_in);
477 par_chunks(&mut xc, pool, &|_, c| {
478 for v in c {
479 *v = v.clamp(lo, hi);
480 }
481 });
482 &xc[..]
483 } else {
484 x
485 };
486 let t0 = std::time::Instant::now();
487 let mut y = self.w.apply(x, n, pool);
488 prof::add(&prof::LIN, t0);
489 if self.lo_out.is_finite() || self.hi_out.is_finite() {
490 let (lo, hi) = (self.lo_out, self.hi_out);
491 par_chunks(&mut y, pool, &|_, c| {
492 for v in c {
493 *v = v.clamp(lo, hi);
494 }
495 });
496 }
497 y
498 }
499}
500
501struct Ffn {
502 pre: Vec<f32>,
503 l1: ClipLin,
504 l2: ClipLin,
505 post: Vec<f32>,
506}
507
508struct ALayer {
509 ff1: Ffn,
510 ff2: Ffn,
511 norm_pre_attn: Vec<f32>,
512 norm_post_attn: Vec<f32>,
513 norm_out: Vec<f32>,
514 q: ClipLin,
515 k: ClipLin,
516 v: ClipLin,
517 post: ClipLin,
518 q_dim: Vec<f32>,
520 rel: Vec<f32>,
522 lc_pre: Vec<f32>,
523 lc_start: ClipLin,
524 lc_dw: Vec<f32>,
526 lc_norm: Vec<f32>,
527 lc_end: ClipLin,
528}
529
530pub struct AudioTower {
532 frontend: MelFrontend,
533 conv0: Vec<f32>,
535 ln0: Vec<f32>,
536 conv1: Vec<f32>,
538 ln1: Vec<f32>,
539 c0: usize,
540 c1: usize,
541 in_proj: Mat,
542 layers: Vec<ALayer>,
543 out_proj: Mat,
544 out_bias: Vec<f32>,
545 embed: Mat,
546 hidden: usize,
547 heads: usize,
548 head_dim: usize,
549 left: usize,
551 kernel: usize,
552 softcap: f32,
553 residual_weight: f32,
554 q_scale: f32,
555 k_scale: f32,
556 pub out_dim: usize,
558 pub audio_token: u32,
559 pub boa_token: u32,
560 pub eoa_token: u32,
561 pool: Option<Arc<Pool>>,
562 busy: Mutex<()>,
563}
564
565pub fn has_audio(model: &CmfModel) -> bool {
567 model
568 .tensor("audio_tower.subsample_conv_projection.layer0.conv.weight")
569 .is_some()
570 && model
571 .tensor("embed_audio.embedding_projection.weight")
572 .is_some()
573}
574
575impl AudioTower {
576 pub fn load(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
580 if !crate::egemma2::is_embedding_gemma2(model) {
581 return Err("not an EmbeddingGemma 2 file".into());
582 }
583 if !has_audio(model) {
584 return Err(
585 "this EmbeddingGemma 2 file has no audio tower (packed with CMF_EGEMMA2_TEXT_ONLY=1?)"
586 .into(),
587 );
588 }
589 let prov = model
590 .header
591 .provenance
592 .clone()
593 .unwrap_or(serde_json::Value::Null);
594 let eg = &prov["embedding_gemma2"];
595 let cfg = &eg["config"];
596 let ac = &cfg["audio_config"];
597 check_frontend(&eg["preprocessor"])?;
598 let gu = |k: &str, d: u64| ac.get(k).and_then(|v| v.as_u64()).unwrap_or(d) as usize;
599 let gf = |k: &str, d: f64| ac.get(k).and_then(|v| v.as_f64()).unwrap_or(d);
600 let hidden = gu("hidden_size", 1024);
601 let heads = gu("num_attention_heads", 8);
602 let n_layers = gu("num_hidden_layers", 12);
603 let chunk = gu("attention_chunk_size", 12);
604 let ctx_left = gu("attention_context_left", 13);
605 let ctx_right = gu("attention_context_right", 0);
606 if ctx_right != 0 {
607 return Err(format!(
608 "audio: attention_context_right {ctx_right} is not supported (0 in the release)"
609 ));
610 }
611 let head_dim = hidden / heads;
612 let left = ctx_left.saturating_sub(1).max(1);
613 let dequant = !std::env::var("CMF_EGEMMA2_LOWMEM").is_ok_and(|v| v == "1");
614
615 let n_ts = hidden / 2;
620 let incr = (10000f64).ln() / (n_ts.max(2) - 1) as f64;
621 let inv: Vec<f32> = (0..n_ts)
622 .map(|i| ((i as f32) * (-incr as f32)).exp())
623 .collect();
624 let n_dist = (chunk + ctx_left - 1 + ctx_right) / 2 + 1;
625 if n_dist < left {
626 return Err("audio: relative position table shorter than the attention span".into());
627 }
628 let mut pe = vec![0f32; n_dist * hidden];
629 for d in 0..n_dist {
630 for (i, &f) in inv.iter().enumerate() {
631 let t = d as f32 * f;
632 pe[d * hidden + i] = t.sin();
633 pe[d * hidden + n_ts + i] = t.cos();
634 }
635 }
636
637 let p0 = "audio_tower.subsample_conv_projection";
638 let conv0 = vecf(model, &format!("{p0}.layer0.conv.weight"))?;
639 let conv1 = vecf(model, &format!("{p0}.layer1.conv.weight"))?;
640 let ln0 = vecf(model, &format!("{p0}.layer0.norm.weight"))?;
641 let ln1 = vecf(model, &format!("{p0}.layer1.norm.weight"))?;
642 let (c0, c1) = (ln0.len(), ln1.len());
643 if conv0.len() != c0 * 9 || conv1.len() != c1 * c0 * 9 {
644 return Err("audio: unexpected subsampling convolution shapes".into());
645 }
646 let in_proj = Mat::load(model, &format!("{p0}.input_proj_linear.weight"), dequant)?;
647 if in_proj.rows() != hidden {
648 return Err("audio: input projection width mismatch".into());
649 }
650
651 let mut layers = Vec::with_capacity(n_layers);
652 for i in 0..n_layers {
653 let p = format!("audio_tower.layers.{i}");
654 let ffn = |f: &str| -> Result<Ffn, String> {
655 Ok(Ffn {
656 pre: vecf(model, &format!("{p}.{f}.pre_layer_norm.weight"))?,
657 l1: ClipLin::load(model, &format!("{p}.{f}.ffw_layer_1"), dequant)?,
658 l2: ClipLin::load(model, &format!("{p}.{f}.ffw_layer_2"), dequant)?,
659 post: vecf(model, &format!("{p}.{f}.post_layer_norm.weight"))?,
660 })
661 };
662 let pds = vecf(model, &format!("{p}.self_attn.per_dim_scale"))?;
663 if pds.len() != head_dim {
664 return Err(format!(
665 "audio layer {i}: per_dim_scale has {} values",
666 pds.len()
667 ));
668 }
669 let q_dim = pds
671 .iter()
672 .map(|&x| {
673 if x > 20.0 {
674 x
675 } else {
676 ((x as f64).exp().ln_1p()) as f32
677 }
678 })
679 .collect();
680 let relk = Mat::load(
681 model,
682 &format!("{p}.self_attn.relative_k_proj.weight"),
683 true,
684 )?;
685 let rel = relk.apply(&pe, n_dist, None);
686 let lc_dw = vecf(model, &format!("{p}.lconv1d.depthwise_conv1d.weight"))?;
687 layers.push(ALayer {
688 ff1: ffn("feed_forward1")?,
689 ff2: ffn("feed_forward2")?,
690 norm_pre_attn: vecf(model, &format!("{p}.norm_pre_attn.weight"))?,
691 norm_post_attn: vecf(model, &format!("{p}.norm_post_attn.weight"))?,
692 norm_out: vecf(model, &format!("{p}.norm_out.weight"))?,
693 q: ClipLin::load(model, &format!("{p}.self_attn.q_proj"), dequant)?,
694 k: ClipLin::load(model, &format!("{p}.self_attn.k_proj"), dequant)?,
695 v: ClipLin::load(model, &format!("{p}.self_attn.v_proj"), dequant)?,
696 post: ClipLin::load(model, &format!("{p}.self_attn.post"), dequant)?,
697 q_dim,
698 rel,
699 lc_pre: vecf(model, &format!("{p}.lconv1d.pre_layer_norm.weight"))?,
700 lc_start: ClipLin::load(model, &format!("{p}.lconv1d.linear_start"), dequant)?,
701 lc_dw,
702 lc_norm: vecf(model, &format!("{p}.lconv1d.conv_norm.weight"))?,
703 lc_end: ClipLin::load(model, &format!("{p}.lconv1d.linear_end"), dequant)?,
704 });
705 }
706 let kernel = layers
707 .first()
708 .map(|l| l.lc_dw.len() / hidden)
709 .unwrap_or(gu("conv_kernel_size", 5));
710 let out_proj = Mat::load(model, "audio_tower.output_proj.weight", dequant)?;
711 let out_bias = vecf(model, "audio_tower.output_proj.bias")?;
712 let embed = Mat::load(model, "embed_audio.embedding_projection.weight", true)?;
713 let tok = |k: &str, d: u32| cfg.get(k).and_then(|v| v.as_u64()).map_or(d, |v| v as u32);
714 Ok(AudioTower {
715 frontend: MelFrontend::new(),
716 conv0,
717 ln0,
718 conv1,
719 ln1,
720 c0,
721 c1,
722 in_proj,
723 layers,
724 out_bias,
725 out_dim: embed.rows(),
726 out_proj,
727 embed,
728 hidden,
729 heads,
730 head_dim,
731 left,
732 kernel,
733 softcap: gf("attention_logit_cap", 50.0) as f32,
734 residual_weight: gf("residual_weight", 0.5) as f32,
735 q_scale: ((head_dim as f64).powf(-0.5) / std::f64::consts::LN_2) as f32,
736 k_scale: ((1.0 + std::f64::consts::E).ln() / std::f64::consts::LN_2) as f32,
737 audio_token: tok("audio_token_id", AUDIO_TOKEN),
738 boa_token: tok("boa_token_id", BOA_TOKEN),
739 eoa_token: tok("eoa_token_index", tok("eoa_token_id", EOA_TOKEN)),
740 pool,
741 busy: Mutex::new(()),
742 })
743 }
744
745 pub fn log_mel(&self, wave: &[f32]) -> (Vec<f32>, usize) {
747 self.frontend.log_mel(wave, self.pool.as_deref())
748 }
749
750 pub fn soft_tokens(&self, clips: &[&[f32]]) -> Vec<Vec<f32>> {
753 let _g = self.busy.lock().unwrap_or_else(|e| e.into_inner());
754 let t0 = std::time::Instant::now();
755 let mels: Vec<(Vec<f32>, usize)> = clips.iter().map(|c| self.log_mel(c)).collect();
756 prof::add(&prof::MEL, t0);
757 let mut out = Vec::with_capacity(clips.len());
758 let mut start = 0usize;
759 while start < mels.len() {
760 let mut end = start;
761 let mut toks = 0usize;
762 while end < mels.len() {
763 let t = mels[end].1.div_ceil(4);
764 if end > start && toks + t > PACK_SOFT {
765 break;
766 }
767 toks += t;
768 end += 1;
769 }
770 let part: Vec<(&[f32], usize)> =
771 mels[start..end].iter().map(|(m, n)| (&m[..], *n)).collect();
772 out.extend(self.encode_locked(&part, None));
773 start = end;
774 }
775 out
776 }
777
778 pub fn encode_mels(
782 &self,
783 mels: &[(&[f32], usize)],
784 trace: Option<&mut Vec<Vec<f32>>>,
785 ) -> Vec<Vec<f32>> {
786 let _g = self.busy.lock().unwrap_or_else(|e| e.into_inner());
787 self.encode_locked(mels, trace)
788 }
789
790 fn encode_locked(
791 &self,
792 mels: &[(&[f32], usize)],
793 mut trace: Option<&mut Vec<Vec<f32>>>,
794 ) -> Vec<Vec<f32>> {
795 let pool = self.pool.as_deref();
796 let d = self.hidden;
797 let mut x = Vec::new();
799 let mut lens = Vec::with_capacity(mels.len());
800 for &(m, frames) in mels {
801 let t0 = std::time::Instant::now();
802 let (sub, t) = self.subsample(m, frames, pool);
803 prof::add(&prof::SUB, t0);
804 x.extend_from_slice(&sub);
805 lens.push(t);
806 }
807 let n: usize = lens.iter().sum();
808 let out_dim = self.out_dim;
809 if n == 0 {
810 return lens.iter().map(|_| Vec::new()).collect();
811 }
812 let mut pos = Vec::with_capacity(n);
814 for &l in &lens {
815 pos.extend(0..l);
816 }
817 let t0 = std::time::Instant::now();
818 let mut h = self.in_proj.apply(&x, n, pool);
819 prof::add(&prof::LIN, t0);
820 drop(x);
821 if let Some(t) = trace.as_deref_mut() {
822 t.push(h.clone());
823 }
824 for l in &self.layers {
825 self.ffn(&l.ff1, &mut h, n, pool);
826 let a = rms_rows(&h, Some(&l.norm_pre_attn), d, pool);
828 let o = self.attention(l, &a, n, &pos, pool);
829 add_normed(&mut h, &o, &l.norm_post_attn, 1.0, d, pool);
830 let a = rms_rows(&h, Some(&l.lc_pre), d, pool);
832 let mut g = l.lc_start.apply(&a, n, pool);
833 let gl = glu(&mut g, n, d, pool);
834 drop(g);
835 let mut c = self.dwconv(&gl, &l.lc_dw, n, &pos, pool);
836 drop(gl);
837 norm_silu(&mut c, &l.lc_norm, d, pool);
838 let e = l.lc_end.apply(&c, n, pool);
839 par_chunks(&mut h, pool, &|off, c| {
840 for (hv, ev) in c.iter_mut().zip(&e[off..]) {
841 *hv = ev + *hv;
842 }
843 });
844 self.ffn(&l.ff2, &mut h, n, pool);
845 h = rms_rows(&h, Some(&l.norm_out), d, pool);
846 if let Some(t) = trace.as_deref_mut() {
847 t.push(h.clone());
848 }
849 }
850 let t0 = std::time::Instant::now();
851 let mut o = self.out_proj.apply(&h, n, pool);
852 prof::add(&prof::LIN, t0);
853 let od = self.out_bias.len();
854 let bias = &self.out_bias;
855 par_chunks(&mut o, pool, &|off, c| {
856 for (i, v) in c.iter_mut().enumerate() {
857 *v += bias[(off + i) % od];
858 }
859 });
860 if let Some(t) = trace.as_deref_mut() {
861 t.push(o.clone());
862 }
863 let on = rms_rows(&o, None, od, pool);
864 let t0 = std::time::Instant::now();
865 let e = self.embed.apply(&on, n, pool);
866 prof::add(&prof::LIN, t0);
867 let mut res = Vec::with_capacity(lens.len());
868 let mut off = 0usize;
869 for &l in &lens {
870 res.push(e[off * out_dim..(off + l) * out_dim].to_vec());
871 off += l;
872 }
873 res
874 }
875
876 fn subsample(&self, mel: &[f32], frames: usize, pool: Option<&Pool>) -> (Vec<f32>, usize) {
880 let (c0, c1) = (self.c0, self.c1);
881 let f0 = N_MELS;
882 let t1 = frames.div_ceil(2);
883 let f1 = f0.div_ceil(2);
884 let mut y0 = vec![0f32; t1 * f1 * c0];
886 let dst = Shared(y0.as_mut_ptr());
887 rows(pool, t1, &|s, e| {
888 let o = unsafe { dst.at(s * f1 * c0, (e - s) * f1 * c0) };
889 let mut patch = [0f32; 9];
890 for t in s..e {
891 for f in 0..f1 {
892 for dt in 0..3 {
893 for df in 0..3 {
894 let (ti, fi) = ((2 * t + dt) as isize - 1, (2 * f + df) as isize - 1);
895 patch[dt * 3 + df] = if ti >= 0
896 && (ti as usize) < frames
897 && fi >= 0
898 && (fi as usize) < f0
899 {
900 mel[ti as usize * f0 + fi as usize]
901 } else {
902 0.0
903 };
904 }
905 }
906 let row = &mut o[((t - s) * f1 + f) * c0..((t - s) * f1 + f + 1) * c0];
907 for (c, r) in row.iter_mut().enumerate() {
908 let w = &self.conv0[c * 9..(c + 1) * 9];
909 let mut acc = 0f32;
910 for k in 0..9 {
911 acc += w[k] * patch[k];
912 }
913 *r = acc;
914 }
915 layer_norm_relu(row, &self.ln0);
916 }
917 }
918 });
919 let t2 = t1.div_ceil(2);
921 let f2 = f1.div_ceil(2);
922 let kk = c0 * 9;
923 let mut y1 = vec![0f32; t2 * f2 * c1];
924 const TB: usize = 32;
925 let mut col = vec![0f32; TB * f2 * kk];
926 for tb in (0..t2).step_by(TB) {
927 let te = (tb + TB).min(t2);
928 let nr = (te - tb) * f2;
929 let cp = Shared(col.as_mut_ptr());
930 rows(pool, te - tb, &|s, e| {
931 let cc = unsafe { cp.at(s * f2 * kk, (e - s) * f2 * kk) };
932 for tt in s..e {
933 let t = tb + tt;
934 for f in 0..f2 {
935 let r = &mut cc[((tt - s) * f2 + f) * kk..((tt - s) * f2 + f + 1) * kk];
936 for dt in 0..3 {
937 for df in 0..3 {
938 let (ti, fi) =
939 ((2 * t + dt) as isize - 1, (2 * f + df) as isize - 1);
940 let inside =
941 ti >= 0 && (ti as usize) < t1 && fi >= 0 && (fi as usize) < f1;
942 for ci in 0..c0 {
943 r[ci * 9 + dt * 3 + df] = if inside {
944 y0[(ti as usize * f1 + fi as usize) * c0 + ci]
945 } else {
946 0.0
947 };
948 }
949 }
950 }
951 }
952 }
953 });
954 crate::egemma2::gemm_nt(
955 &col[..nr * kk],
956 &self.conv1,
957 &mut y1[tb * f2 * c1..te * f2 * c1],
958 nr,
959 kk,
960 c1,
961 pool,
962 );
963 }
964 for row in y1.chunks_exact_mut(c1) {
965 layer_norm_relu(row, &self.ln1);
966 }
967 (y1, t2)
968 }
969
970 fn ffn(&self, f: &Ffn, h: &mut [f32], n: usize, pool: Option<&Pool>) {
972 let d = self.hidden;
973 let a = rms_rows(h, Some(&f.pre), d, pool);
974 let mut u = f.l1.apply(&a, n, pool);
975 silu_inplace(&mut u, pool);
976 let y = f.l2.apply(&u, n, pool);
977 add_normed(h, &y, &f.post, self.residual_weight, d, pool);
978 }
979
980 fn attention(
982 &self,
983 l: &ALayer,
984 a: &[f32],
985 n: usize,
986 pos: &[usize],
987 pool: Option<&Pool>,
988 ) -> Vec<f32> {
989 let (nh, hd, d) = (self.heads, self.head_dim, self.hidden);
990 let mut q = l.q.apply(a, n, pool);
991 let mut k = l.k.apply(a, n, pool);
992 let v = l.v.apply(a, n, pool);
993 let (qs, ks) = (self.q_scale, self.k_scale);
994 let qd = &l.q_dim;
995 par_chunks(&mut q, pool, &|off, c| {
996 for (i, x) in c.iter_mut().enumerate() {
997 *x = (*x * qs) * qd[(off + i) % hd];
998 }
999 });
1000 par_chunks(&mut k, pool, &|_, c| {
1001 for x in c {
1002 *x *= ks;
1003 }
1004 });
1005 let t_core = std::time::Instant::now();
1006 let mut out = vec![0f32; n * d];
1007 let dst = Shared(out.as_mut_ptr());
1008 let (left, cap) = (self.left, self.softcap);
1009 rows(pool, n, &|s, e| {
1010 let o = unsafe { dst.at(s * d, (e - s) * d) };
1011 let mut w = vec![0f32; left];
1012 for t in s..e {
1013 let span = (pos[t] + 1).min(left);
1014 for hh in 0..nh {
1015 let qv = &q[t * d + hh * hd..t * d + (hh + 1) * hd];
1016 let mut mx = f32::NEG_INFINITY;
1017 for (dist, wd) in w.iter_mut().enumerate().take(span) {
1018 let kv = &k[(t - dist) * d + hh * hd..(t - dist) * d + (hh + 1) * hd];
1019 let rv = &l.rel[dist * d + hh * hd..dist * d + (hh + 1) * hd];
1020 let (mut ac, mut bd) = (0f32, 0f32);
1021 for j in 0..hd {
1022 ac += qv[j] * kv[j];
1023 bd += qv[j] * rv[j];
1024 }
1025 let z = ((ac + bd) / cap).tanh() * cap;
1026 *wd = z;
1027 mx = mx.max(z);
1028 }
1029 let mut den = 0f32;
1030 for wd in w.iter_mut().take(span) {
1031 *wd = (*wd - mx).exp();
1032 den += *wd;
1033 }
1034 let ov = &mut o[(t - s) * d + hh * hd..(t - s) * d + (hh + 1) * hd];
1035 ov.fill(0.0);
1036 for (dist, &wd) in w.iter().enumerate().take(span) {
1037 let p = wd / den;
1038 let vv = &v[(t - dist) * d + hh * hd..(t - dist) * d + (hh + 1) * hd];
1039 for j in 0..hd {
1040 ov[j] += p * vv[j];
1041 }
1042 }
1043 }
1044 }
1045 });
1046 prof::add(&prof::ATT, t_core);
1047 l.post.apply(&out, n, pool)
1048 }
1049
1050 fn dwconv(
1052 &self,
1053 x: &[f32],
1054 w: &[f32],
1055 n: usize,
1056 pos: &[usize],
1057 pool: Option<&Pool>,
1058 ) -> Vec<f32> {
1059 let (d, kn) = (self.hidden, self.kernel);
1060 let mut out = vec![0f32; n * d];
1061 let dst = Shared(out.as_mut_ptr());
1062 rows(pool, n, &|s, e| {
1063 let o = unsafe { dst.at(s * d, (e - s) * d) };
1064 for t in s..e {
1065 let row = &mut o[(t - s) * d..(t - s + 1) * d];
1066 for (k, back) in (0..kn).zip((0..kn).rev()) {
1067 if back > pos[t] {
1068 continue;
1069 }
1070 let src = &x[(t - back) * d..(t - back + 1) * d];
1071 for c in 0..d {
1072 row[c] += w[c * kn + k] * src[c];
1073 }
1074 }
1075 }
1076 });
1077 out
1078 }
1079}
1080
1081fn check_frontend(pp: &serde_json::Value) -> Result<(), String> {
1083 if pp.is_null() {
1084 return Ok(());
1085 }
1086 let f = |k: &str| pp.get(k).and_then(|v| v.as_f64());
1087 let want: [(&str, f64); 9] = [
1088 ("feature_size", N_MELS as f64),
1089 ("sampling_rate", SAMPLE_RATE as f64),
1090 ("frame_length", FRAME as f64),
1091 ("hop_length", HOP as f64),
1092 ("fft_length", N_FFT as f64),
1093 ("mel_floor", MEL_FLOOR),
1094 ("preemphasis", 0.0),
1095 ("dither", 0.0),
1096 ("input_scale_factor", 1.0),
1097 ];
1098 for (k, v) in want {
1099 if let Some(got) = f(k) {
1100 if (got - v).abs() > 1e-9 {
1101 return Err(format!(
1102 "audio: preprocessor {k} = {got}, this build implements {v}"
1103 ));
1104 }
1105 }
1106 }
1107 for k in ["per_bin_mean", "per_bin_stddev"] {
1108 if pp.get(k).is_some_and(|v| !v.is_null()) {
1109 return Err(format!("audio: preprocessor {k} is set; not implemented"));
1110 }
1111 }
1112 Ok(())
1113}
1114
1115fn par_chunks(x: &mut [f32], pool: Option<&Pool>, f: &(dyn Fn(usize, &mut [f32]) + Sync)) {
1117 let n = x.len();
1118 let grain = 16384usize;
1119 let dst = Shared(x.as_mut_ptr());
1120 rows(pool, n.div_ceil(grain), &|s, e| {
1121 let (lo, hi) = (s * grain, (e * grain).min(n));
1122 f(lo, unsafe { dst.at(lo, hi - lo) });
1123 });
1124}
1125
1126fn layer_norm_relu(x: &mut [f32], w: &[f32]) {
1128 let n = x.len() as f64;
1129 let mean = x.iter().map(|&v| v as f64).sum::<f64>() / n;
1130 let var = x
1131 .iter()
1132 .map(|&v| {
1133 let c = v as f64 - mean;
1134 c * c
1135 })
1136 .sum::<f64>()
1137 / n;
1138 let inv = 1.0 / (var + EPS).sqrt();
1139 for (v, &g) in x.iter_mut().zip(w) {
1140 *v = (((*v as f64 - mean) * inv) as f32 * g).max(0.0);
1141 }
1142}
1143
1144fn add_normed(h: &mut [f32], y: &[f32], w: &[f32], scale: f32, d: usize, pool: Option<&Pool>) {
1146 let n = h.len() / d;
1147 let dst = Shared(h.as_mut_ptr());
1148 rows(pool, n, &|s, e| {
1149 let hh = unsafe { dst.at(s * d, (e - s) * d) };
1150 let mut tmp = vec![0f32; d];
1151 for (hrow, yrow) in hh.chunks_exact_mut(d).zip(y[s * d..e * d].chunks_exact(d)) {
1152 rms_into(yrow, Some(w), &mut tmp);
1153 for (a, &b) in hrow.iter_mut().zip(&tmp) {
1154 *a += b * scale;
1155 }
1156 }
1157 });
1158}
1159
1160#[inline]
1161fn sigmoid(x: f32) -> f32 {
1162 1.0 / (1.0 + (-x).exp())
1163}
1164
1165fn silu_inplace(x: &mut [f32], pool: Option<&Pool>) {
1167 let n = x.len();
1168 let grain = 16384usize;
1169 let dst = Shared(x.as_mut_ptr());
1170 rows(pool, n.div_ceil(grain), &|s, e| {
1171 let (lo, hi) = (s * grain, (e * grain).min(n));
1172 let r = unsafe { dst.at(lo, hi - lo) };
1173 silu_slice(r);
1174 });
1175}
1176
1177#[inline]
1178fn silu_slice(x: &mut [f32]) {
1179 #[allow(unused_mut)]
1180 let mut i = 0usize;
1181 #[cfg(target_arch = "aarch64")]
1182 unsafe {
1184 use core::arch::aarch64::*;
1185 let one = vdupq_n_f32(1.0);
1186 while i + 4 <= x.len() {
1187 let v = vld1q_f32(x.as_ptr().add(i));
1188 let e = crate::attention::vexpq_f32(vnegq_f32(v));
1189 vst1q_f32(x.as_mut_ptr().add(i), vdivq_f32(v, vaddq_f32(one, e)));
1190 i += 4;
1191 }
1192 }
1193 for v in x[i..].iter_mut() {
1194 *v *= sigmoid(*v);
1195 }
1196}
1197
1198fn glu(x: &mut [f32], n: usize, d: usize, pool: Option<&Pool>) -> Vec<f32> {
1200 let mut out = vec![0f32; n * d];
1201 let dst = Shared(out.as_mut_ptr());
1202 rows(pool, n, &|s, e| {
1203 let o = unsafe { dst.at(s * d, (e - s) * d) };
1204 for t in s..e {
1205 let (a, b) = x[t * 2 * d..(t + 1) * 2 * d].split_at(d);
1206 for ((r, &av), &bv) in o[(t - s) * d..(t - s + 1) * d].iter_mut().zip(a).zip(b) {
1207 *r = av * sigmoid(bv);
1208 }
1209 }
1210 });
1211 out
1212}
1213
1214fn norm_silu(x: &mut [f32], w: &[f32], d: usize, pool: Option<&Pool>) {
1216 let n = x.len() / d;
1217 let dst = Shared(x.as_mut_ptr());
1218 rows(pool, n, &|s, e| {
1219 let xx = unsafe { dst.at(s * d, (e - s) * d) };
1220 let mut tmp = vec![0f32; d];
1221 for row in xx.chunks_exact_mut(d) {
1222 rms_into(row, Some(w), &mut tmp);
1223 row.copy_from_slice(&tmp);
1224 silu_slice(row);
1225 }
1226 });
1227}
1228
1229#[derive(Clone, Debug, Default)]
1240pub struct AudioInput {
1241 pub text: Option<TextInput>,
1242 pub clips: Vec<Vec<f32>>,
1243}
1244
1245pub fn input_ids(
1248 enc: &EmbeddingGemma2,
1249 tower_tokens: (u32, u32, u32),
1250 input: &AudioInput,
1251 n_soft: &[usize],
1252) -> Result<Vec<u32>, String> {
1253 let (audio, boa, eoa) = tower_tokens;
1254 let expand = |out: &mut Vec<u32>, n: usize| {
1255 out.push(boa);
1256 out.extend(std::iter::repeat_n(audio, n));
1257 out.push(eoa);
1258 };
1259 let mut ids = vec![enc.bos];
1260 let bare = TextInput::plain(vec![PLACEHOLDER; n_soft.len()].join(" "));
1262 let t = input.text.as_ref().unwrap_or(&bare);
1263 let body = enc.tokenizer().encode(&enc.format(t)?);
1264 let found = body.iter().filter(|&&x| x == audio).count();
1265 if found != n_soft.len() {
1266 return Err(format!(
1267 "the text holds {found} {PLACEHOLDER} placeholder(s) for {} audio clip(s)",
1268 n_soft.len()
1269 ));
1270 }
1271 let mut k = 0usize;
1272 for &x in &body {
1273 if x == audio {
1274 expand(&mut ids, n_soft[k]);
1275 k += 1;
1276 } else {
1277 ids.push(x);
1278 }
1279 }
1280 ids.push(enc.eos);
1281 if ids.len() > enc.max_tokens {
1282 return Err(format!(
1283 "{} tokens > the model's {} token context (audio is 25 tokens a second)",
1284 ids.len(),
1285 enc.max_tokens
1286 ));
1287 }
1288 Ok(ids)
1289}
1290
1291pub fn embed_audio_inputs(
1293 enc: &EmbeddingGemma2,
1294 tower: &AudioTower,
1295 inputs: &[AudioInput],
1296) -> Result<Vec<Vec<f32>>, String> {
1297 let toks = (tower.audio_token, tower.boa_token, tower.eoa_token);
1298 let mut seqs = Vec::with_capacity(inputs.len());
1300 for (i, inp) in inputs.iter().enumerate() {
1301 if inp.clips.is_empty() && inp.text.is_none() {
1302 return Err(format!("input {i}: no audio and no text"));
1303 }
1304 let n: Vec<usize> = inp.clips.iter().map(|c| num_tokens(c.len())).collect();
1305 seqs.push(input_ids(enc, toks, inp, &n).map_err(|e| format!("input {i}: {e}"))?);
1306 }
1307 let clips: Vec<&[f32]> = inputs
1308 .iter()
1309 .flat_map(|i| i.clips.iter().map(|c| &c[..]))
1310 .collect();
1311 let t_tower = std::time::Instant::now();
1312 let soft = tower.soft_tokens(&clips);
1313 let tower_ms = t_tower.elapsed().as_secs_f64() * 1e3;
1314 crate::egemma2::prof::take(&crate::egemma2::prof::LIN);
1317 let t_text = std::time::Instant::now();
1318 let od = tower.out_dim;
1319 let d = enc.hidden();
1320 if od != d {
1321 return Err(format!(
1322 "audio soft tokens are {od}-d, the text encoder is {d}-d"
1323 ));
1324 }
1325 let mut merged: Vec<Vec<f32>> = Vec::with_capacity(inputs.len());
1327 let mut next = 0usize;
1328 for (inp, ids) in inputs.iter().zip(&seqs) {
1329 let mut x = enc.embed_rows(std::slice::from_ref(ids));
1330 let mut rows_it = ids
1331 .iter()
1332 .enumerate()
1333 .filter(|(_, t)| **t == tower.audio_token)
1334 .map(|(r, _)| r);
1335 for _ in 0..inp.clips.len() {
1336 let s = &soft[next];
1337 next += 1;
1338 for tok in s.chunks_exact(od) {
1339 let r = rows_it.next().ok_or("audio: placeholder count mismatch")?;
1340 x[r * d..(r + 1) * d].copy_from_slice(tok);
1341 }
1342 }
1343 if rows_it.next().is_some() {
1344 return Err("audio: placeholder count mismatch".into());
1345 }
1346 merged.push(x);
1347 }
1348 let mut out = Vec::with_capacity(inputs.len());
1350 let mut start = 0usize;
1351 while start < seqs.len() {
1352 let mut end = start;
1353 let mut tot = 0usize;
1354 while end < seqs.len()
1355 && (end == start || tot + seqs[end].len() <= crate::egemma2::MAX_TOKENS)
1356 {
1357 tot += seqs[end].len();
1358 end += 1;
1359 }
1360 let mut x = Vec::with_capacity(tot * d);
1361 for m in &merged[start..end] {
1362 x.extend_from_slice(m);
1363 }
1364 let lens: Vec<usize> = seqs[start..end].iter().map(|s| s.len()).collect();
1365 out.extend(enc.embed_merged(x, &lens));
1366 start = end;
1367 }
1368 if prof::on() {
1369 let (mel, sub, lin, att) = (
1370 prof::take(&prof::MEL),
1371 prof::take(&prof::SUB),
1372 prof::take(&prof::LIN),
1373 prof::take(&prof::ATT),
1374 );
1375 let secs: f64 = clips.iter().map(|c| c.len() as f64).sum::<f64>() / SAMPLE_RATE as f64;
1376 let toks: usize = soft.iter().map(|s| s.len() / od).sum();
1377 eprintln!(
1378 "egemma2 audio: {} clip(s), {secs:.2} s, {toks} soft tokens — tower {tower_ms:.1} ms \
1379 (log-mel {mel:.1}, subsampling {sub:.1}, projections {lin:.1}, attention core {att:.1}, \
1380 rest {:.1}); text {:.1} ms",
1381 clips.len(),
1382 tower_ms - mel - sub - lin - att,
1383 t_text.elapsed().as_secs_f64() * 1e3
1384 );
1385 }
1386 Ok(out)
1387}
1388
1389#[cfg(test)]
1390mod tests {
1391 use super::*;
1392
1393 #[test]
1394 fn token_counts_follow_the_processor() {
1395 for (n, f, t) in [
1397 (100, 0, 0),
1398 (161, 1, 1),
1399 (321, 2, 1),
1400 (480, 2, 1),
1401 (1000, 6, 2),
1402 (16000, 99, 25),
1403 (72825, 455, 114),
1404 (480000, 2999, 750),
1405 (480001, 2999, 750),
1406 (560000, 2999, 750),
1407 ] {
1408 assert_eq!(num_frames(n), f, "frames of {n}");
1409 assert_eq!(num_tokens(n), t, "tokens of {n}");
1410 }
1411 }
1412
1413 #[test]
1414 fn mel_bank_is_htk_triangles() {
1415 let m = mel_filter_bank();
1416 let n_freq = N_FFT / 2 + 1;
1417 assert_eq!(m.len(), n_freq * N_MELS);
1418 assert!(m[..N_MELS].iter().all(|&v| v == 0.0));
1420 for b in 0..N_MELS {
1422 let col: Vec<f64> = (0..n_freq).map(|k| m[k * N_MELS + b]).collect();
1423 assert!(col.iter().all(|&v| (0.0..=1.0).contains(&v)));
1424 }
1425 assert!((0..n_freq).all(|k| m[k * N_MELS] == 0.0));
1429 assert!((m[N_MELS + 1] - 0.766_007_66).abs() < 1e-6);
1430 assert!((m[N_MELS + 2] - 0.233_992_32).abs() < 1e-6);
1431 }
1432
1433 #[test]
1434 fn silence_is_the_mel_floor() {
1435 let fe = MelFrontend::new();
1436 let (mel, n) = fe.log_mel(&vec![0f32; 16000], None);
1437 assert_eq!(n, 99);
1438 let floor = (MEL_FLOOR).ln() as f32;
1439 assert!(mel.iter().all(|&v| (v - floor).abs() < 1e-6));
1440 }
1441
1442 #[test]
1443 fn a_tone_lands_in_its_band() {
1444 let fe = MelFrontend::new();
1445 let f = 1000.0f64;
1446 let wave: Vec<f32> = (0..16000)
1447 .map(|i| (0.5 * (2.0 * std::f64::consts::PI * f * i as f64 / 16000.0).sin()) as f32)
1448 .collect();
1449 let (mel, n) = fe.log_mel(&wave, None);
1450 let row = &mel[(n / 2) * N_MELS..(n / 2 + 1) * N_MELS];
1451 let best = row
1452 .iter()
1453 .enumerate()
1454 .max_by(|a, b| a.1.total_cmp(b.1))
1455 .unwrap()
1456 .0;
1457 let hz2mel = |f: f64| 2595.0 * (1.0 + f / 700.0).log10();
1459 let centre = |b: usize| {
1460 let m = hz2mel(8000.0) * (b + 1) as f64 / 129.0;
1461 700.0 * (10f64.powf(m / 2595.0) - 1.0)
1462 };
1463 assert!(
1464 (centre(best) - f).abs() < 60.0,
1465 "band {best} at {}",
1466 centre(best)
1467 );
1468 }
1469
1470 #[test]
1471 fn stereo_mixes_down_and_rates_resample() {
1472 let wav = crate::mimo_audio::Wav {
1473 sample_rate: 16000,
1474 channels: vec![vec![1.0, 0.0, 0.5], vec![0.0, 1.0, 0.5]],
1475 };
1476 assert_eq!(to_mono_16k(&wav), vec![0.5, 0.5, 0.5]);
1477 let wav = crate::mimo_audio::Wav {
1478 sample_rate: 48000,
1479 channels: vec![vec![0.25; 4800]],
1480 };
1481 let m = to_mono_16k(&wav);
1482 assert_eq!(m.len(), 1600);
1483 assert!((m[800] - 0.25).abs() < 1e-4);
1484 }
1485
1486 #[test]
1487 fn resampling_keeps_a_tone_and_its_length() {
1488 for (from, f0) in [
1489 (8000u32, 1000.0f64),
1490 (44100, 3000.0),
1491 (48000, 7000.0),
1492 (22050, 440.0),
1493 ] {
1494 let n = from as usize; let x: Vec<f32> = (0..n)
1496 .map(|i| (2.0 * std::f64::consts::PI * f0 * i as f64 / from as f64).sin() as f32)
1497 .collect();
1498 let y = resample_hq(&x, from, SAMPLE_RATE);
1499 assert_eq!(y.len(), 16000, "{from}");
1500 let err = (2000..14000)
1502 .map(|i| {
1503 let want = (2.0 * std::f64::consts::PI * f0 * i as f64 / 16000.0).sin();
1504 (y[i] as f64 - want).abs()
1505 })
1506 .fold(0f64, f64::max);
1507 assert!(err < 1e-4, "{from} Hz: max err {err}");
1508 }
1509 let x: Vec<f32> = (0..48000)
1511 .map(|i| (2.0 * std::f64::consts::PI * 9000.0 * i as f64 / 48000.0).sin() as f32)
1512 .collect();
1513 let y = resample_hq(&x, 48000, SAMPLE_RATE);
1514 let rms = (y[2000..14000]
1515 .iter()
1516 .map(|v| (*v as f64).powi(2))
1517 .sum::<f64>()
1518 / 12000.0)
1519 .sqrt();
1520 assert!(rms < 1e-4, "alias rms {rms}");
1521 let x: Vec<f32> = (0..44099).map(|i| ((i % 97) as f32 / 97.0) - 0.5).collect();
1523 let y = resample_hq(&x, 44099, SAMPLE_RATE);
1524 assert_eq!(y.len(), (16000u64 * 44099).div_ceil(44099) as usize);
1525 }
1526}