use crate::error::{Error, Result};
use crate::resample::resample_poly;
use crate::wav::WavF64;
pub const MODEL_RATE: u32 = 48_000;
const STD_EPS: f32 = 1e-9;
pub trait AudioTextScorer {
fn scores(&mut self, clips: &[Vec<f32>], texts: &[String]) -> Result<Vec<f32>>;
}
pub fn model_clip(wav: &WavF64) -> Result<Vec<f32>> {
let c = usize::from(wav.channels.max(1));
let n = c as f32;
let mono: Vec<f32> = wav
.samples
.chunks_exact(c)
.map(|frame| frame.iter().fold(0.0f32, |acc, &v| acc + v as f32) / n)
.collect();
let mut clip = resample_poly(&mono, MODEL_RATE as usize, wav.sample_rate as usize)?;
if clip.len() < MODEL_RATE as usize {
clip.resize(MODEL_RATE as usize, 0.0);
}
Ok(clip)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rank {
pub rank: usize,
pub heard: String,
}
pub fn rank(scores: &[f32], own: &[usize], texts: &[String]) -> Result<Vec<Rank>> {
let (rows, cols) = (own.len(), texts.len());
if scores.len() != rows * cols || own.iter().any(|&o| o >= cols) {
return Err(Error::InvalidArgument {
arg: "scores",
reason: format!("{} scores for {rows} clips and {cols} texts", scores.len()),
});
}
let n = rows as f32;
let mut z = scores.to_vec();
for j in 0..cols {
let mean = (0..rows).fold(0.0f32, |acc, i| acc + scores[i * cols + j]) / n;
let var = (0..rows).fold(0.0f32, |acc, i| {
let d = scores[i * cols + j] - mean;
acc + d * d
}) / n;
let std = var.sqrt() + STD_EPS;
for i in 0..rows {
z[i * cols + j] = (scores[i * cols + j] - mean) / std;
}
}
let mut out = Vec::with_capacity(rows);
for (i, &own_text) in own.iter().enumerate() {
let row = &z[i * cols..(i + 1) * cols];
let mut order: Vec<usize> = (0..cols).collect();
order.sort_by(|&a, &b| row[b].total_cmp(&row[a]));
let place = order.iter().position(|&t| t == own_text).unwrap_or(cols);
out.push(Rank {
rank: place + 1,
heard: order.first().map(|&t| texts[t].clone()).unwrap_or_default(),
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn texts(n: usize) -> Vec<String> {
(0..n).map(|i| format!("t{i}")).collect()
}
#[test]
fn a_text_liked_for_everything_gains_no_edge() {
let scores = [0.5f32, 0.9, 0.1, 0.89];
let r = rank(&scores, &[0, 1], &texts(2)).expect("rank");
assert_eq!(
r[0],
Rank {
rank: 1,
heard: "t0".into()
}
);
assert_eq!(
r[1],
Rank {
rank: 1,
heard: "t1".into()
}
);
}
#[test]
fn a_misheard_clip_names_the_winner() {
let scores = [0.9f32, 0.1, 0.0, 0.2, 0.8, 0.0, 0.1, 0.9, 0.05];
let r = rank(&scores, &[0, 2, 2], &texts(3)).expect("rank");
assert_eq!(
r[1],
Rank {
rank: 3,
heard: "t1".into()
}
);
}
#[test]
fn mismatched_sizes_are_refused() {
assert!(rank(&[0.0; 3], &[0, 1], &texts(2)).is_err());
assert!(rank(&[0.0; 4], &[0, 2], &texts(2)).is_err());
}
#[test]
fn a_short_clip_pads_to_one_second() {
let wav = WavF64 {
samples: vec![0.5, 0.25, -0.5, -0.25],
sample_rate: MODEL_RATE,
channels: 2,
};
let clip = model_clip(&wav).expect("clip");
assert_eq!(clip.len(), MODEL_RATE as usize);
assert_eq!(&clip[..3], &[0.375, -0.375, 0.0]);
}
}