1use anyhow::{anyhow, ensure, Result};
2
3use crate::runtime::{Model, Tensor};
4
5const CHUNK_SIZE: usize = 1500;
7const BORDER_SIZE: usize = 6;
9const STRIDE: usize = CHUNK_SIZE - 2 * BORDER_SIZE;
11
12pub struct BeatPredictor<M: Model> {
19 model: M,
20}
21
22impl<M: Model> BeatPredictor<M> {
23 pub fn new(model: M) -> Self {
25 Self { model }
26 }
27
28 pub fn model_mut(&mut self) -> &mut M {
30 &mut self.model
31 }
32
33 pub fn predict(&mut self, mel: &Tensor) -> Result<(Vec<f32>, Vec<f32>)> {
38 ensure!(
39 mel.shape.len() == 3 && mel.shape[0] == 1 && mel.shape[2] == 128,
40 "Expected mel shape [1, T, 128], got {:?}",
41 mel.shape
42 );
43
44 let full_time = mel.shape[1];
45 let starts = generate_starts(full_time);
46
47 let mut beat_logits = vec![-1000.0f32; full_time];
49 let mut downbeat_logits = vec![-1000.0f32; full_time];
50
51 for &start in starts.iter().rev() {
55 let chunk = extract_chunk(mel, start);
56 let chunk_time = chunk.shape[1];
57
58 let mut outputs = self.model.run(&[("spectrogram", &chunk)])?;
59
60 let beat = extract_output(&mut outputs, "beat", "beat_logits")?;
61 let downbeat = extract_output(&mut outputs, "downbeat", "downbeat_logits")?;
62
63 let valid_beat = &beat.data[BORDER_SIZE..chunk_time - BORDER_SIZE];
65 let valid_downbeat = &downbeat.data[BORDER_SIZE..chunk_time - BORDER_SIZE];
66
67 let write_start = (start + BORDER_SIZE as i32) as usize;
69 for (i, (&b, &d)) in valid_beat.iter().zip(valid_downbeat.iter()).enumerate() {
70 let dest = write_start + i;
71 if dest < full_time {
72 beat_logits[dest] = b;
73 downbeat_logits[dest] = d;
74 }
75 }
76 }
77
78 Ok((beat_logits, downbeat_logits))
79 }
80}
81
82fn extract_output(
84 outputs: &mut std::collections::HashMap<String, Tensor>,
85 primary: &str,
86 fallback: &str,
87) -> Result<Tensor> {
88 if let Some(t) = outputs.remove(primary) {
89 return Ok(t);
90 }
91 if let Some(t) = outputs.remove(fallback) {
92 return Ok(t);
93 }
94 Err(anyhow!(
95 "Model missing output '{}' (also tried '{}'). Available: {:?}",
96 primary,
97 fallback,
98 outputs.keys().collect::<Vec<_>>()
99 ))
100}
101
102fn generate_starts(full_time: usize) -> Vec<i32> {
107 let mut starts = Vec::new();
108 let mut pos = -(BORDER_SIZE as i32);
109 let limit = full_time as i32 - BORDER_SIZE as i32;
110
111 while pos < limit {
112 starts.push(pos);
113 pos += STRIDE as i32;
114 }
115
116 if full_time > STRIDE {
119 if let Some(last) = starts.last_mut() {
120 *last = full_time as i32 - (CHUNK_SIZE as i32 - BORDER_SIZE as i32);
121 }
122 }
123
124 starts
125}
126
127fn extract_chunk(mel: &Tensor, start: i32) -> Tensor {
134 let full_time = mel.shape[1];
135 let n_mels = mel.shape[2]; let actual_start = start.max(0) as usize;
138 let actual_end = ((start + CHUNK_SIZE as i32) as usize).min(full_time);
139 let pad_left = (-start).max(0) as usize;
140 let n_frames = actual_end - actual_start;
141
142 let pad_right =
144 0.max((start + CHUNK_SIZE as i32 - full_time as i32).min(BORDER_SIZE as i32)) as usize;
145
146 let chunk_time = pad_left + n_frames + pad_right;
147 let mut data = vec![0.0f32; chunk_time * n_mels];
148
149 for t in actual_start..actual_end {
151 let src_offset = t * n_mels;
152 let dst_t = pad_left + (t - actual_start);
153 let dst_offset = dst_t * n_mels;
154 data[dst_offset..dst_offset + n_mels]
155 .copy_from_slice(&mel.data[src_offset..src_offset + n_mels]);
156 }
157
158 Tensor {
159 shape: vec![1, chunk_time, n_mels],
160 data,
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_generate_starts_short() {
170 let starts = generate_starts(100);
172 assert_eq!(starts, vec![-6]);
173 }
174
175 #[test]
176 fn test_generate_starts_exact_chunk() {
177 let starts = generate_starts(1500);
180 assert_eq!(starts.len(), 2);
181 assert_eq!(starts[0], -6);
182 assert_eq!(starts[1], 6);
184 }
185
186 #[test]
187 fn test_generate_starts_two_chunks() {
188 let starts = generate_starts(2000);
190 assert_eq!(starts.len(), 2);
191 assert_eq!(starts[0], -6);
192 assert_eq!(starts[1], 506);
194 }
195
196 #[test]
197 fn test_generate_starts_long() {
198 let starts = generate_starts(5000);
200 assert_eq!(starts[0], -6);
201 assert_eq!(starts.len(), 4);
203 assert_eq!(starts[1], 1482);
204 assert_eq!(starts[2], 2970);
205 assert_eq!(starts[3], 3506);
206 }
207
208 #[test]
209 fn test_generate_starts_coverage() {
210 for full_time in [50, 100, 500, 1488, 1500, 2000, 3000, 5000, 7800] {
212 let starts = generate_starts(full_time);
213 let mut covered = vec![false; full_time];
214 for &start in &starts {
215 let pad_left = (-start).max(0) as usize;
216 let actual_end = ((start + CHUNK_SIZE as i32) as usize).min(full_time);
217 let actual_start = start.max(0) as usize;
218 let n_frames = actual_end - actual_start;
219 let pad_right = 0
220 .max((start + CHUNK_SIZE as i32 - full_time as i32).min(BORDER_SIZE as i32))
221 as usize;
222 let chunk_time = pad_left + n_frames + pad_right;
223 let write_start = (start + BORDER_SIZE as i32).max(0) as usize;
224 let write_end =
225 ((start as usize).wrapping_add(chunk_time) - BORDER_SIZE).min(full_time);
226 for i in write_start..write_end {
227 covered[i] = true;
228 }
229 }
230 assert!(
231 covered.iter().all(|&c| c),
232 "Not all frames covered for full_time={full_time}. First uncovered: {}",
233 covered.iter().position(|&c| !c).unwrap()
234 );
235 }
236 }
237
238 #[test]
239 fn test_extract_chunk_short_audio() {
240 let n_mels = 128;
243 let full_time = 100;
244 let mel = Tensor {
245 shape: vec![1, full_time, n_mels],
246 data: vec![1.0; full_time * n_mels],
247 };
248
249 let chunk = extract_chunk(&mel, -6);
250 assert_eq!(chunk.shape, vec![1, 112, n_mels]);
252
253 for t in 0..6 {
255 assert_eq!(
256 chunk.data[t * n_mels],
257 0.0,
258 "Expected zero padding at t={t}"
259 );
260 }
261 assert_eq!(chunk.data[6 * n_mels], 1.0);
263 assert_eq!(chunk.data[105 * n_mels], 1.0);
265 for t in 106..112 {
267 assert_eq!(
268 chunk.data[t * n_mels],
269 0.0,
270 "Expected zero padding at t={t}"
271 );
272 }
273 }
274
275 #[test]
276 fn test_extract_chunk_long_audio_first() {
277 let n_mels = 128;
279 let full_time = 5000;
280 let mel = Tensor {
281 shape: vec![1, full_time, n_mels],
282 data: vec![1.0; full_time * n_mels],
283 };
284
285 let chunk = extract_chunk(&mel, -6);
286 assert_eq!(chunk.shape, vec![1, CHUNK_SIZE, n_mels]);
287
288 for t in 0..6 {
290 assert_eq!(chunk.data[t * n_mels], 0.0);
291 }
292 assert_eq!(chunk.data[6 * n_mels], 1.0);
294 }
295
296 #[test]
297 fn test_extract_chunk_long_audio_middle() {
298 let n_mels = 128;
300 let full_time = 5000;
301 let mel = Tensor {
302 shape: vec![1, full_time, n_mels],
303 data: vec![1.0; full_time * n_mels],
304 };
305
306 let chunk = extract_chunk(&mel, 100);
307 assert_eq!(chunk.shape, vec![1, CHUNK_SIZE, n_mels]);
308
309 assert!(chunk.data.iter().all(|&v| v == 1.0));
311 }
312}