use anyhow::{anyhow, ensure, Result};
use crate::runtime::{Model, Tensor};
const CHUNK_SIZE: usize = 1500;
const BORDER_SIZE: usize = 6;
const STRIDE: usize = CHUNK_SIZE - 2 * BORDER_SIZE;
pub struct BeatPredictor<M: Model> {
model: M,
}
impl<M: Model> BeatPredictor<M> {
pub fn new(model: M) -> Self {
Self { model }
}
pub fn model_mut(&mut self) -> &mut M {
&mut self.model
}
pub fn predict(&mut self, mel: &Tensor) -> Result<(Vec<f32>, Vec<f32>)> {
ensure!(
mel.shape.len() == 3 && mel.shape[0] == 1 && mel.shape[2] == 128,
"Expected mel shape [1, T, 128], got {:?}",
mel.shape
);
let full_time = mel.shape[1];
let starts = generate_starts(full_time);
let mut beat_logits = vec![-1000.0f32; full_time];
let mut downbeat_logits = vec![-1000.0f32; full_time];
for &start in starts.iter().rev() {
let chunk = extract_chunk(mel, start);
let chunk_time = chunk.shape[1];
let mut outputs = self.model.run(&[("spectrogram", &chunk)])?;
let beat = extract_output(&mut outputs, "beat", "beat_logits")?;
let downbeat = extract_output(&mut outputs, "downbeat", "downbeat_logits")?;
let valid_beat = &beat.data[BORDER_SIZE..chunk_time - BORDER_SIZE];
let valid_downbeat = &downbeat.data[BORDER_SIZE..chunk_time - BORDER_SIZE];
let write_start = (start + BORDER_SIZE as i32) as usize;
for (i, (&b, &d)) in valid_beat.iter().zip(valid_downbeat.iter()).enumerate() {
let dest = write_start + i;
if dest < full_time {
beat_logits[dest] = b;
downbeat_logits[dest] = d;
}
}
}
Ok((beat_logits, downbeat_logits))
}
}
fn extract_output(
outputs: &mut std::collections::HashMap<String, Tensor>,
primary: &str,
fallback: &str,
) -> Result<Tensor> {
if let Some(t) = outputs.remove(primary) {
return Ok(t);
}
if let Some(t) = outputs.remove(fallback) {
return Ok(t);
}
Err(anyhow!(
"Model missing output '{}' (also tried '{}'). Available: {:?}",
primary,
fallback,
outputs.keys().collect::<Vec<_>>()
))
}
fn generate_starts(full_time: usize) -> Vec<i32> {
let mut starts = Vec::new();
let mut pos = -(BORDER_SIZE as i32);
let limit = full_time as i32 - BORDER_SIZE as i32;
while pos < limit {
starts.push(pos);
pos += STRIDE as i32;
}
if full_time > STRIDE {
if let Some(last) = starts.last_mut() {
*last = full_time as i32 - (CHUNK_SIZE as i32 - BORDER_SIZE as i32);
}
}
starts
}
fn extract_chunk(mel: &Tensor, start: i32) -> Tensor {
let full_time = mel.shape[1];
let n_mels = mel.shape[2];
let actual_start = start.max(0) as usize;
let actual_end = ((start + CHUNK_SIZE as i32) as usize).min(full_time);
let pad_left = (-start).max(0) as usize;
let n_frames = actual_end - actual_start;
let pad_right =
0.max((start + CHUNK_SIZE as i32 - full_time as i32).min(BORDER_SIZE as i32)) as usize;
let chunk_time = pad_left + n_frames + pad_right;
let mut data = vec![0.0f32; chunk_time * n_mels];
for t in actual_start..actual_end {
let src_offset = t * n_mels;
let dst_t = pad_left + (t - actual_start);
let dst_offset = dst_t * n_mels;
data[dst_offset..dst_offset + n_mels]
.copy_from_slice(&mel.data[src_offset..src_offset + n_mels]);
}
Tensor {
shape: vec![1, chunk_time, n_mels],
data,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_starts_short() {
let starts = generate_starts(100);
assert_eq!(starts, vec![-6]);
}
#[test]
fn test_generate_starts_exact_chunk() {
let starts = generate_starts(1500);
assert_eq!(starts.len(), 2);
assert_eq!(starts[0], -6);
assert_eq!(starts[1], 6);
}
#[test]
fn test_generate_starts_two_chunks() {
let starts = generate_starts(2000);
assert_eq!(starts.len(), 2);
assert_eq!(starts[0], -6);
assert_eq!(starts[1], 506);
}
#[test]
fn test_generate_starts_long() {
let starts = generate_starts(5000);
assert_eq!(starts[0], -6);
assert_eq!(starts.len(), 4);
assert_eq!(starts[1], 1482);
assert_eq!(starts[2], 2970);
assert_eq!(starts[3], 3506);
}
#[test]
fn test_generate_starts_coverage() {
for full_time in [50, 100, 500, 1488, 1500, 2000, 3000, 5000, 7800] {
let starts = generate_starts(full_time);
let mut covered = vec![false; full_time];
for &start in &starts {
let pad_left = (-start).max(0) as usize;
let actual_end = ((start + CHUNK_SIZE as i32) as usize).min(full_time);
let actual_start = start.max(0) as usize;
let n_frames = actual_end - actual_start;
let pad_right = 0
.max((start + CHUNK_SIZE as i32 - full_time as i32).min(BORDER_SIZE as i32))
as usize;
let chunk_time = pad_left + n_frames + pad_right;
let write_start = (start + BORDER_SIZE as i32).max(0) as usize;
let write_end =
((start as usize).wrapping_add(chunk_time) - BORDER_SIZE).min(full_time);
for i in write_start..write_end {
covered[i] = true;
}
}
assert!(
covered.iter().all(|&c| c),
"Not all frames covered for full_time={full_time}. First uncovered: {}",
covered.iter().position(|&c| !c).unwrap()
);
}
}
#[test]
fn test_extract_chunk_short_audio() {
let n_mels = 128;
let full_time = 100;
let mel = Tensor {
shape: vec![1, full_time, n_mels],
data: vec![1.0; full_time * n_mels],
};
let chunk = extract_chunk(&mel, -6);
assert_eq!(chunk.shape, vec![1, 112, n_mels]);
for t in 0..6 {
assert_eq!(
chunk.data[t * n_mels],
0.0,
"Expected zero padding at t={t}"
);
}
assert_eq!(chunk.data[6 * n_mels], 1.0);
assert_eq!(chunk.data[105 * n_mels], 1.0);
for t in 106..112 {
assert_eq!(
chunk.data[t * n_mels],
0.0,
"Expected zero padding at t={t}"
);
}
}
#[test]
fn test_extract_chunk_long_audio_first() {
let n_mels = 128;
let full_time = 5000;
let mel = Tensor {
shape: vec![1, full_time, n_mels],
data: vec![1.0; full_time * n_mels],
};
let chunk = extract_chunk(&mel, -6);
assert_eq!(chunk.shape, vec![1, CHUNK_SIZE, n_mels]);
for t in 0..6 {
assert_eq!(chunk.data[t * n_mels], 0.0);
}
assert_eq!(chunk.data[6 * n_mels], 1.0);
}
#[test]
fn test_extract_chunk_long_audio_middle() {
let n_mels = 128;
let full_time = 5000;
let mel = Tensor {
shape: vec![1, full_time, n_mels],
data: vec![1.0; full_time * n_mels],
};
let chunk = extract_chunk(&mel, 100);
assert_eq!(chunk.shape, vec![1, CHUNK_SIZE, n_mels]);
assert!(chunk.data.iter().all(|&v| v == 1.0));
}
}