use std::sync::Arc;
use crate::core::preanalysis::PreAnalysisArtifact;
use crate::engine::stage::OnsetEvent;
const KEEP_BEHIND_FRAMES: f64 = 1_024.0;
const HORIZON_FRAMES: f64 = 2_048.0;
pub(crate) const MAX_EVENTS: usize = 16;
#[derive(Debug)]
pub(crate) struct TransientCursor {
artifact: Arc<PreAnalysisArtifact>,
onset_cursor: usize,
beat_cursor: usize,
events: [OnsetEvent; MAX_EVENTS],
len: usize,
}
impl TransientCursor {
pub(crate) fn new(artifact: Arc<PreAnalysisArtifact>) -> Self {
Self {
artifact,
onset_cursor: 0,
beat_cursor: 0,
events: [OnsetEvent::default(); MAX_EVENTS],
len: 0,
}
}
pub(crate) fn reset(&mut self) {
self.onset_cursor = 0;
self.beat_cursor = 0;
self.len = 0;
}
pub(crate) fn advance(
&mut self,
stage_now: f64,
mut map_track_to_stage: impl FnMut(u64) -> Option<f64>,
) -> &[OnsetEvent] {
self.len = 0;
let onsets = &self.artifact.transient_onsets;
while self.onset_cursor < onsets.len() {
match map_track_to_stage(onsets[self.onset_cursor] as u64) {
Some(stage_frame) if stage_frame < stage_now - KEEP_BEHIND_FRAMES => {
self.onset_cursor += 1;
}
_ => break,
}
}
let beats = &self.artifact.beat_positions;
while self.beat_cursor < beats.len() {
match map_track_to_stage(beats[self.beat_cursor] as u64) {
Some(stage_frame) if stage_frame < stage_now - KEEP_BEHIND_FRAMES => {
self.beat_cursor += 1;
}
_ => break,
}
}
let mut idx = self.onset_cursor;
while idx < onsets.len() && self.len < MAX_EVENTS {
let Some(stage_frame) = map_track_to_stage(onsets[idx] as u64) else {
break;
};
if stage_frame > stage_now + HORIZON_FRAMES {
break;
}
self.events[self.len] = OnsetEvent {
stage_frame,
strength: self.artifact.strength_at(idx),
beat: false,
};
self.len += 1;
idx += 1;
}
let mut idx = self.beat_cursor;
while idx < beats.len() && self.len < MAX_EVENTS {
let Some(stage_frame) = map_track_to_stage(beats[idx] as u64) else {
break;
};
if stage_frame > stage_now + HORIZON_FRAMES {
break;
}
self.events[self.len] = OnsetEvent {
stage_frame,
strength: 1.0,
beat: true,
};
self.len += 1;
idx += 1;
}
&self.events[..self.len]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn artifact(onsets: Vec<usize>, beats: Vec<usize>) -> Arc<PreAnalysisArtifact> {
Arc::new(PreAnalysisArtifact {
transient_onsets: onsets.clone(),
transient_strengths: vec![0.9; onsets.len()],
beat_positions: beats,
sample_rate: 44_100,
bpm: 128.0,
confidence: 0.9,
..Default::default()
})
}
#[test]
fn publishes_events_in_window_and_drops_passed_ones() {
let mut cursor = TransientCursor::new(artifact(vec![1_000, 3_000, 50_000], vec![]));
let events = cursor.advance(1_500.0, |track| Some(track as f64));
assert_eq!(events.len(), 2);
assert_eq!(events[0].stage_frame, 1_000.0);
assert_eq!(events[1].stage_frame, 3_000.0);
let events = cursor.advance(10_000.0, |track| Some(track as f64));
assert!(events.is_empty());
}
#[test]
fn remapping_moves_events_with_the_tempo_ride() {
let mut cursor = TransientCursor::new(artifact(vec![10_000], vec![]));
let events = cursor.advance(8_000.0, |track| Some(track as f64 / 1.05));
assert_eq!(events.len(), 1);
let first = events[0].stage_frame;
assert!((first - 9_523.8).abs() < 1.0);
let events = cursor.advance(8_032.0, |track| Some(track as f64 / 1.02));
assert_eq!(events.len(), 1);
assert!((events[0].stage_frame - 9_803.9).abs() < 1.0);
}
#[test]
fn beats_are_flagged_and_unmappable_positions_hold_the_cursor() {
let mut cursor = TransientCursor::new(artifact(vec![2_000], vec![3_000]));
let events = cursor.advance(1_000.0, |track| {
if track <= 2_500 {
Some(track as f64)
} else {
None }
});
assert_eq!(events.len(), 1);
assert!(!events[0].beat);
let events = cursor.advance(1_032.0, |track| Some(track as f64));
assert_eq!(events.len(), 2);
assert!(events[1].beat);
assert_eq!(events[1].stage_frame, 3_000.0);
}
#[test]
fn seek_skips_permanently_passed_events() {
let mut cursor = TransientCursor::new(artifact(vec![1_000, 90_000], vec![]));
let events = cursor.advance(100.0, |track| {
if track < 88_000 {
Some(f64::NEG_INFINITY)
} else {
Some(track as f64 - 88_000.0)
}
});
assert_eq!(events.len(), 1);
assert_eq!(events[0].stage_frame, 2_000.0);
}
}