use std::time::Duration;
use crate::player::SectionBounds;
pub struct SectionLoopTrigger {
next_trigger: Option<Duration>,
}
impl SectionLoopTrigger {
pub fn new() -> Self {
Self { next_trigger: None }
}
pub fn check(
&mut self,
section: &SectionBounds,
elapsed: Duration,
margin: Duration,
) -> Option<Duration> {
let section_duration = section.end_time.saturating_sub(section.start_time);
if section_duration.is_zero() {
return None;
}
let trigger = *self.next_trigger.get_or_insert(section.end_time);
if elapsed + margin >= trigger {
self.next_trigger = Some(trigger + section_duration);
Some(trigger)
} else {
None
}
}
pub fn reset(&mut self) {
self.next_trigger = None;
}
}
impl Default for SectionLoopTrigger {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, PartialEq)]
pub enum LoopPoll {
NoSection,
Waiting(SectionBounds),
Triggered(SectionBounds),
SectionCleared,
}
pub struct SectionLoopMonitor {
trigger: SectionLoopTrigger,
cached_section: Option<SectionBounds>,
}
impl SectionLoopMonitor {
pub fn new() -> Self {
Self {
trigger: SectionLoopTrigger::new(),
cached_section: None,
}
}
pub fn cached_section(&self) -> Option<&SectionBounds> {
self.cached_section.as_ref()
}
pub fn poll(
&mut self,
active_section: &parking_lot::RwLock<Option<SectionBounds>>,
elapsed: Duration,
) -> LoopPoll {
let section = active_section.read().clone();
if let Some(section) = section {
self.cached_section = Some(section.clone());
let crossfade_margin = crate::audio::crossfade::DEFAULT_CROSSFADE_DURATION;
if self
.trigger
.check(§ion, elapsed, crossfade_margin)
.is_some()
{
LoopPoll::Triggered(section)
} else {
LoopPoll::Waiting(section)
}
} else {
if self.cached_section.take().is_some() {
self.trigger.reset();
LoopPoll::SectionCleared
} else {
self.trigger.reset();
LoopPoll::NoSection
}
}
}
pub fn reset(&mut self) {
self.trigger.reset();
self.cached_section = None;
}
}
impl Default for SectionLoopMonitor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_section(start_secs: u64, end_secs: u64) -> SectionBounds {
SectionBounds {
name: "test".to_string(),
start_time: Duration::from_secs(start_secs),
end_time: Duration::from_secs(end_secs),
}
}
#[test]
fn stays_on_grid() {
let section = make_section(10, 18); let section_duration = Duration::from_secs(8);
let margin = crate::audio::crossfade::DEFAULT_CROSSFADE_DURATION;
let mut trigger = SectionLoopTrigger::new();
let iterations = 100u32;
for i in 0..iterations {
let expected_trigger = section.end_time + section_duration * i;
let elapsed = expected_trigger - margin;
let result = trigger.check(§ion, elapsed, margin);
assert_eq!(
result,
Some(expected_trigger),
"Iteration {}: expected trigger at {:?}",
i,
expected_trigger
);
}
let expected_next = section.end_time + section_duration * iterations;
let too_early = expected_next - margin - Duration::from_secs(1);
assert_eq!(trigger.check(§ion, too_early, margin), None);
}
#[test]
fn no_fire_before_time() {
let section = make_section(10, 18);
let margin = Duration::from_millis(5);
let mut trigger = SectionLoopTrigger::new();
assert_eq!(
trigger.check(§ion, Duration::from_secs(5), margin),
None
);
let just_short = section.end_time - margin - Duration::from_millis(1);
assert_eq!(trigger.check(§ion, just_short, margin), None);
let at_threshold = section.end_time - margin;
assert_eq!(
trigger.check(§ion, at_threshold, margin),
Some(section.end_time)
);
}
#[test]
fn resets_cleanly() {
let section = make_section(10, 18);
let margin = Duration::from_millis(5);
let mut trigger = SectionLoopTrigger::new();
let elapsed = section.end_time - margin;
assert!(trigger.check(§ion, elapsed, margin).is_some());
trigger.reset();
let elapsed = section.end_time - margin;
assert_eq!(
trigger.check(§ion, elapsed, margin),
Some(section.end_time),
"After reset, trigger should re-initialise from section.end_time"
);
}
#[test]
fn handles_zero_duration_section() {
let section = make_section(10, 10); let margin = Duration::from_millis(5);
let mut trigger = SectionLoopTrigger::new();
assert_eq!(
trigger.check(§ion, Duration::from_secs(10), margin),
None
);
assert_eq!(
trigger.check(§ion, Duration::from_secs(100), margin),
None
);
}
#[test]
fn monitor_no_section() {
let active = parking_lot::RwLock::new(None);
let mut monitor = SectionLoopMonitor::new();
assert_eq!(monitor.poll(&active, Duration::ZERO), LoopPoll::NoSection);
}
#[test]
fn monitor_waiting_then_triggered() {
let section = make_section(10, 18);
let active = parking_lot::RwLock::new(Some(section.clone()));
let mut monitor = SectionLoopMonitor::new();
let result = monitor.poll(&active, Duration::from_secs(5));
assert_eq!(result, LoopPoll::Waiting(section.clone()));
let margin = crate::audio::crossfade::DEFAULT_CROSSFADE_DURATION;
let elapsed = section.end_time - margin;
let result = monitor.poll(&active, elapsed);
assert_eq!(result, LoopPoll::Triggered(section));
}
#[test]
fn monitor_section_cleared() {
let section = make_section(10, 18);
let active = parking_lot::RwLock::new(Some(section.clone()));
let mut monitor = SectionLoopMonitor::new();
let _ = monitor.poll(&active, Duration::from_secs(5));
assert!(monitor.cached_section().is_some());
*active.write() = None;
let result = monitor.poll(&active, Duration::from_secs(5));
assert_eq!(result, LoopPoll::SectionCleared);
let result = monitor.poll(&active, Duration::from_secs(5));
assert_eq!(result, LoopPoll::NoSection);
}
}