1use anyhow::{anyhow, ensure, Result};
2
3use crate::runtime::{InferenceSession, Tensor};
4
5const CHUNK_SIZE: usize = 1500;
7const BORDER_SIZE: usize = 6;
9const STRIDE: usize = CHUNK_SIZE - 2 * BORDER_SIZE;
11
12pub struct BeatInference<S: InferenceSession> {
19 session: S,
20}
21
22impl<S: InferenceSession> BeatInference<S> {
23 pub fn new(session: S) -> Self {
25 Self { session }
26 }
27
28 pub fn session_mut(&mut self) -> &mut S {
30 &mut self.session
31 }
32
33 pub fn process(&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
57 let mut outputs = self.session.run(&[("spectrogram", &chunk)])?;
58
59 let beat = extract_output(&mut outputs, "beat", "beat_logits")?;
60 let downbeat = extract_output(&mut outputs, "downbeat", "downbeat_logits")?;
61
62 let valid_beat = &beat.data[BORDER_SIZE..CHUNK_SIZE - BORDER_SIZE];
64 let valid_downbeat = &downbeat.data[BORDER_SIZE..CHUNK_SIZE - BORDER_SIZE];
65
66 let write_start = (start + BORDER_SIZE as i32) as usize;
68 for i in 0..STRIDE {
69 let dest = write_start + i;
70 if dest < full_time {
71 beat_logits[dest] = valid_beat[i];
72 downbeat_logits[dest] = valid_downbeat[i];
73 }
74 }
75 }
76
77 Ok((beat_logits, downbeat_logits))
78 }
79}
80
81fn extract_output(
83 outputs: &mut std::collections::HashMap<String, Tensor>,
84 primary: &str,
85 fallback: &str,
86) -> Result<Tensor> {
87 if let Some(t) = outputs.remove(primary) {
88 return Ok(t);
89 }
90 if let Some(t) = outputs.remove(fallback) {
91 return Ok(t);
92 }
93 Err(anyhow!(
94 "Model missing output '{}' (also tried '{}'). Available: {:?}",
95 primary,
96 fallback,
97 outputs.keys().collect::<Vec<_>>()
98 ))
99}
100
101fn generate_starts(full_time: usize) -> Vec<i32> {
106 let mut starts = Vec::new();
107 let mut pos = -(BORDER_SIZE as i32);
108 let limit = full_time as i32 - BORDER_SIZE as i32;
109
110 while pos < limit {
111 starts.push(pos);
112 pos += STRIDE as i32;
113 }
114
115 if full_time > STRIDE {
118 if let Some(last) = starts.last_mut() {
119 *last = full_time as i32 - (CHUNK_SIZE as i32 - BORDER_SIZE as i32);
120 }
121 }
122
123 starts
124}
125
126fn extract_chunk(mel: &Tensor, start: i32) -> Tensor {
130 let full_time = mel.shape[1];
131 let n_mels = mel.shape[2]; let actual_start = start.max(0) as usize;
134 let actual_end = ((start + CHUNK_SIZE as i32) as usize).min(full_time);
135 let pad_left = (-start).max(0) as usize;
136
137 let mut data = vec![0.0f32; CHUNK_SIZE * n_mels];
138
139 for t in actual_start..actual_end {
141 let src_offset = t * n_mels;
142 let dst_t = pad_left + (t - actual_start);
143 let dst_offset = dst_t * n_mels;
144 data[dst_offset..dst_offset + n_mels]
145 .copy_from_slice(&mel.data[src_offset..src_offset + n_mels]);
146 }
147
148 Tensor {
149 shape: vec![1, CHUNK_SIZE, n_mels],
150 data,
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_generate_starts_short() {
160 let starts = generate_starts(100);
162 assert_eq!(starts, vec![-6]);
163 }
164
165 #[test]
166 fn test_generate_starts_exact_chunk() {
167 let starts = generate_starts(1500);
170 assert_eq!(starts.len(), 2);
171 assert_eq!(starts[0], -6);
172 assert_eq!(starts[1], 6);
174 }
175
176 #[test]
177 fn test_generate_starts_two_chunks() {
178 let starts = generate_starts(2000);
180 assert_eq!(starts.len(), 2);
181 assert_eq!(starts[0], -6);
182 assert_eq!(starts[1], 506);
184 }
185
186 #[test]
187 fn test_generate_starts_long() {
188 let starts = generate_starts(5000);
190 assert_eq!(starts[0], -6);
191 assert_eq!(starts.len(), 4);
193 assert_eq!(starts[1], 1482);
194 assert_eq!(starts[2], 2970);
195 assert_eq!(starts[3], 3506);
196 }
197
198 #[test]
199 fn test_generate_starts_coverage() {
200 for full_time in [50, 100, 500, 1488, 1500, 2000, 3000, 5000, 7800] {
202 let starts = generate_starts(full_time);
203 let mut covered = vec![false; full_time];
204 for &start in &starts {
205 let write_start = (start + BORDER_SIZE as i32).max(0) as usize;
206 let write_end =
207 ((start + CHUNK_SIZE as i32 - BORDER_SIZE as i32) as usize).min(full_time);
208 for i in write_start..write_end {
209 covered[i] = true;
210 }
211 }
212 assert!(
213 covered.iter().all(|&c| c),
214 "Not all frames covered for full_time={full_time}. First uncovered: {}",
215 covered.iter().position(|&c| !c).unwrap()
216 );
217 }
218 }
219
220 #[test]
221 fn test_extract_chunk_first() {
222 let n_mels = 128;
224 let full_time = 100;
225 let mel = Tensor {
226 shape: vec![1, full_time, n_mels],
227 data: vec![1.0; full_time * n_mels],
228 };
229
230 let chunk = extract_chunk(&mel, -6);
231 assert_eq!(chunk.shape, vec![1, CHUNK_SIZE, n_mels]);
232
233 for t in 0..6 {
235 for f in 0..n_mels {
236 assert_eq!(
237 chunk.data[t * n_mels + f],
238 0.0,
239 "Expected zero padding at t={t}, f={f}"
240 );
241 }
242 }
243 assert_eq!(chunk.data[6 * n_mels], 1.0);
245 }
246
247 #[test]
248 fn test_extract_chunk_middle() {
249 let n_mels = 128;
251 let full_time = 5000;
252 let mel = Tensor {
253 shape: vec![1, full_time, n_mels],
254 data: vec![1.0; full_time * n_mels],
255 };
256
257 let chunk = extract_chunk(&mel, 100);
258 assert_eq!(chunk.shape, vec![1, CHUNK_SIZE, n_mels]);
259
260 assert!(chunk.data.iter().all(|&v| v == 1.0));
262 }
263}