use std::path::Path;
use crate::error::{Error, Result};
use boostr::model::audio::voxcpm::VoxCpmClient;
use boostr::model::audio::voxcpm::vae::AudioVaeEncoder;
use numr::dtype::DType;
use numr::ops::{BinaryOps, ReduceOps, TensorOps, TypeConversionOps, UnaryOps};
use numr::runtime::Runtime;
use numr::tensor::Tensor;
pub const ENCODER_RATE: u32 = 16_000;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TimbreProxy {
pub latent_cosine: f64,
pub f0_mean_delta_hz: Option<f64>,
pub f0_std_ratio: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct F0Stats {
pub mean_hz: Option<f64>,
pub std_hz: Option<f64>,
}
pub struct TimbreScorer<R: Runtime> {
encoder: AudioVaeEncoder<R>,
}
impl<R: Runtime<DType = DType>> TimbreScorer<R> {
pub fn from_checkpoint(path: &Path, device: &R::Device) -> Result<Self>
where
R::Client: TypeConversionOps<R> + ReduceOps<R> + UnaryOps<R> + BinaryOps<R> + TensorOps<R>,
{
Ok(Self::new(AudioVaeEncoder::<R>::from_checkpoint(
path, device,
)?))
}
pub fn new(encoder: AudioVaeEncoder<R>) -> Self {
Self { encoder }
}
pub fn embed<C: VoxCpmClient<R>>(&self, client: &C, samples_16k: &[f32]) -> Result<Vec<f32>> {
embed_with(&self.encoder, client, samples_16k)
}
}
pub fn embed_with<R, C>(
encoder: &AudioVaeEncoder<R>,
client: &C,
samples_16k: &[f32],
) -> Result<Vec<f32>>
where
R: Runtime<DType = DType>,
C: VoxCpmClient<R>,
{
if samples_16k.is_empty() {
return Err(Error::InvalidArgument {
arg: "samples_16k",
reason: "expected at least 1 sample, got 0".to_string(),
});
}
let wave = Tensor::<R>::from_slice(samples_16k, &[1, 1, samples_16k.len()], client.device())?;
let latent = encoder.forward(client, &wave)?;
let shape = latent.shape().to_vec();
if shape.len() != 3 || shape[0] != 1 || shape[2] == 0 {
return Err(Error::DataError {
reason: format!(
"AudioVAE latent has shape {shape:?}, expected [1, feat_dim, frames > 0]"
),
});
}
let (feat_dim, frames) = (shape[1], shape[2]);
let values: Vec<f32> = latent.contiguous()?.try_to_vec()?;
Ok(mean_over_frames(&values, feat_dim, frames))
}
fn mean_over_frames(values: &[f32], feat_dim: usize, frames: usize) -> Vec<f32> {
(0..feat_dim)
.map(|c| {
let row = &values[c * frames..(c + 1) * frames];
row.iter().map(|&v| f64::from(v)).sum::<f64>() / frames as f64
})
.map(|m| m as f32)
.collect()
}
pub fn cosine(a: &[f32], b: &[f32]) -> f64 {
let mut dot = 0.0f64;
let mut na = 0.0f64;
let mut nb = 0.0f64;
for (&x, &y) in a.iter().zip(b) {
let (x, y) = (f64::from(x), f64::from(y));
dot += x * y;
na += x * x;
nb += y * y;
}
if na <= 0.0 || nb <= 0.0 {
return 0.0;
}
dot / (na.sqrt() * nb.sqrt())
}
pub fn timbre_proxy(
render_embed: &[f32],
reference_embed: &[f32],
render_f0: F0Stats,
reference_f0: F0Stats,
) -> Result<TimbreProxy> {
if render_embed.len() != reference_embed.len() {
return Err(Error::InvalidArgument {
arg: "render_embed",
reason: format!(
"render embedding has {} values, reference has {}",
render_embed.len(),
reference_embed.len()
),
});
}
let f0_mean_delta_hz = match (render_f0.mean_hz, reference_f0.mean_hz) {
(Some(r), Some(f)) => Some(r - f),
_ => None,
};
let f0_std_ratio = match (render_f0.std_hz, reference_f0.std_hz) {
(Some(r), Some(f)) if f > 0.0 => Some(r / f),
_ => None,
};
Ok(TimbreProxy {
latent_cosine: cosine(render_embed, reference_embed),
f0_mean_delta_hz,
f0_std_ratio,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cosine_of_parallel_and_orthogonal_vectors() {
assert!((cosine(&[1.0, 2.0], &[2.0, 4.0]) - 1.0).abs() < 1e-9);
assert!(cosine(&[1.0, 0.0], &[0.0, 1.0]).abs() < 1e-9);
assert_eq!(cosine(&[0.0, 0.0], &[1.0, 1.0]), 0.0);
}
#[test]
fn pooling_averages_each_channel_row() {
let pooled = mean_over_frames(&[1.0, 2.0, 3.0, 10.0, 20.0, 30.0], 2, 3);
assert_eq!(pooled, vec![2.0, 20.0]);
}
#[test]
fn proxy_reports_f0_deltas_and_rejects_length_mismatch() {
let r = F0Stats {
mean_hz: Some(150.0),
std_hz: Some(20.0),
};
let f = F0Stats {
mean_hz: Some(140.0),
std_hz: Some(10.0),
};
let p = timbre_proxy(&[1.0, 1.0], &[1.0, 1.0], r, f).expect("proxy");
assert!((p.latent_cosine - 1.0).abs() < 1e-9);
assert_eq!(p.f0_mean_delta_hz, Some(10.0));
assert_eq!(p.f0_std_ratio, Some(2.0));
let none = F0Stats {
mean_hz: None,
std_hz: None,
};
let p = timbre_proxy(&[1.0], &[1.0], none, f).expect("proxy");
assert_eq!(p.f0_mean_delta_hz, None);
assert_eq!(p.f0_std_ratio, None);
assert!(timbre_proxy(&[1.0], &[1.0, 2.0], r, f).is_err());
}
}