use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
pub const MIN_TEMPO_RATE: f64 = 0.25;
pub const MAX_TEMPO_RATE: f64 = 4.0;
const MAILBOX_SLOTS: usize = 256;
pub const APPLY_ASAP: u64 = u64::MAX;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Param {
TempoRate,
WarmStart,
}
impl Param {
fn from_code(code: u64) -> Option<Self> {
match code {
0 => Some(Param::TempoRate),
1 => Some(Param::WarmStart),
_ => None,
}
}
fn code(self) -> u64 {
match self {
Param::TempoRate => 0,
Param::WarmStart => 1,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ControlEvent {
pub at_frame: u64,
pub param: Param,
pub value: f64,
}
#[derive(Debug)]
struct Slot {
at_frame: AtomicU64,
param: AtomicU64,
value_bits: AtomicU64,
}
impl Slot {
fn new() -> Self {
Self {
at_frame: AtomicU64::new(0),
param: AtomicU64::new(0),
value_bits: AtomicU64::new(0),
}
}
}
#[derive(Debug)]
pub(crate) struct EngineShared {
slots: Box<[Slot]>,
head: AtomicUsize,
tail: AtomicUsize,
dropped_events: AtomicU64,
tempo_latest_bits: AtomicU64,
underrun_frames: AtomicU64,
source_position_bits: AtomicU64,
delivered_frames: AtomicU64,
}
impl EngineShared {
pub(crate) fn new(initial_tempo_rate: f64) -> Arc<Self> {
let slots: Vec<Slot> = (0..MAILBOX_SLOTS).map(|_| Slot::new()).collect();
Arc::new(Self {
slots: slots.into_boxed_slice(),
head: AtomicUsize::new(0),
tail: AtomicUsize::new(0),
dropped_events: AtomicU64::new(0),
tempo_latest_bits: AtomicU64::new(initial_tempo_rate.to_bits()),
underrun_frames: AtomicU64::new(0),
source_position_bits: AtomicU64::new(0.0f64.to_bits()),
delivered_frames: AtomicU64::new(0),
})
}
fn push_event(&self, event: ControlEvent) {
let tail = self.tail.load(Ordering::Relaxed);
let head = self.head.load(Ordering::Acquire);
if tail.wrapping_sub(head) >= MAILBOX_SLOTS {
self.dropped_events.fetch_add(1, Ordering::Relaxed);
return;
}
let slot = &self.slots[tail % MAILBOX_SLOTS];
slot.at_frame.store(event.at_frame, Ordering::Relaxed);
slot.param.store(event.param.code(), Ordering::Relaxed);
slot.value_bits
.store(event.value.to_bits(), Ordering::Relaxed);
self.tail.store(tail.wrapping_add(1), Ordering::Release);
}
pub(crate) fn pop_event(&self) -> Option<ControlEvent> {
let head = self.head.load(Ordering::Relaxed);
let tail = self.tail.load(Ordering::Acquire);
if head == tail {
return None;
}
let slot = &self.slots[head % MAILBOX_SLOTS];
let event = ControlEvent {
at_frame: slot.at_frame.load(Ordering::Relaxed),
param: Param::from_code(slot.param.load(Ordering::Relaxed))?,
value: f64::from_bits(slot.value_bits.load(Ordering::Relaxed)),
};
self.head.store(head.wrapping_add(1), Ordering::Release);
Some(event)
}
pub(crate) fn tempo_latest(&self) -> f64 {
f64::from_bits(self.tempo_latest_bits.load(Ordering::Relaxed))
}
pub(crate) fn store_tempo_latest(&self, rate: f64) {
self.tempo_latest_bits
.store(rate.to_bits(), Ordering::Relaxed);
}
pub(crate) fn add_underrun_frames(&self, frames: u64) {
if frames > 0 {
self.underrun_frames.fetch_add(frames, Ordering::Relaxed);
}
}
pub(crate) fn publish_position(&self, source_pos: f64, delivered_frames: u64) {
self.source_position_bits
.store(source_pos.to_bits(), Ordering::Relaxed);
self.delivered_frames
.store(delivered_frames, Ordering::Relaxed);
}
}
#[inline]
pub fn clamp_tempo_rate(rate: f64) -> f64 {
if rate.is_finite() {
rate.clamp(MIN_TEMPO_RATE, MAX_TEMPO_RATE)
} else {
1.0
}
}
#[derive(Debug)]
pub struct EngineController {
shared: Arc<EngineShared>,
sample_rate: u32,
}
impl EngineController {
pub(crate) fn new(shared: Arc<EngineShared>, sample_rate: u32) -> Self {
Self {
shared,
sample_rate,
}
}
pub fn set_tempo_rate(&self, rate: f64) {
let rate = clamp_tempo_rate(rate);
self.shared
.tempo_latest_bits
.store(rate.to_bits(), Ordering::Relaxed);
self.shared.push_event(ControlEvent {
at_frame: APPLY_ASAP,
param: Param::TempoRate,
value: rate,
});
}
pub fn set_tempo_rate_at(&self, rate: f64, at_output_frame: u64) {
self.shared.push_event(ControlEvent {
at_frame: at_output_frame,
param: Param::TempoRate,
value: clamp_tempo_rate(rate),
});
}
pub fn tempo_rate_target(&self) -> f64 {
self.shared.tempo_latest()
}
pub fn warm_start(&self, preroll_frames: u32) {
self.shared.push_event(ControlEvent {
at_frame: APPLY_ASAP,
param: Param::WarmStart,
value: preroll_frames as f64,
});
}
pub fn source_position(&self) -> f64 {
f64::from_bits(self.shared.source_position_bits.load(Ordering::Relaxed))
}
pub fn delivered_frames(&self) -> u64 {
self.shared.delivered_frames.load(Ordering::Relaxed)
}
pub fn underrun_frames(&self) -> u64 {
self.shared.underrun_frames.load(Ordering::Relaxed)
}
pub fn dropped_events(&self) -> u64 {
self.shared.dropped_events.load(Ordering::Relaxed)
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mailbox_roundtrip_in_order() {
let shared = EngineShared::new(1.0);
let controller = EngineController::new(Arc::clone(&shared), 44_100);
controller.set_tempo_rate(1.05);
controller.set_tempo_rate(0.98);
let first = shared.pop_event().expect("first event");
assert_eq!(first.param, Param::TempoRate);
assert!((first.value - 1.05).abs() < 1e-12);
let second = shared.pop_event().expect("second event");
assert!((second.value - 0.98).abs() < 1e-12);
assert!(shared.pop_event().is_none());
assert_eq!(controller.dropped_events(), 0);
}
#[test]
fn mailbox_overflow_drops_and_counts_but_latest_converges() {
let shared = EngineShared::new(1.0);
let controller = EngineController::new(Arc::clone(&shared), 44_100);
for i in 0..(MAILBOX_SLOTS + 10) {
controller.set_tempo_rate(1.0 + i as f64 * 1e-4);
}
assert_eq!(controller.dropped_events(), 10);
let expected_latest = clamp_tempo_rate(1.0 + (MAILBOX_SLOTS + 9) as f64 * 1e-4);
assert!((shared.tempo_latest() - expected_latest).abs() < 1e-12);
let mut drained = 0;
while shared.pop_event().is_some() {
drained += 1;
}
assert_eq!(drained, MAILBOX_SLOTS);
}
#[test]
fn tempo_rate_clamped() {
assert_eq!(clamp_tempo_rate(9.0), MAX_TEMPO_RATE);
assert_eq!(clamp_tempo_rate(0.0), MIN_TEMPO_RATE);
assert_eq!(clamp_tempo_rate(f64::NAN), 1.0);
assert_eq!(clamp_tempo_rate(f64::INFINITY), 1.0);
assert_eq!(clamp_tempo_rate(1.07), 1.07);
}
#[test]
fn mailbox_cross_thread_smoke() {
let shared = EngineShared::new(1.0);
let controller = EngineController::new(Arc::clone(&shared), 44_100);
let producer = std::thread::spawn(move || {
for i in 0..10_000u32 {
controller.set_tempo_rate(1.0 + (i % 100) as f64 * 1e-3);
}
});
let mut seen = 0u64;
while !producer.is_finished() {
while let Some(event) = shared.pop_event() {
assert!(event.value >= MIN_TEMPO_RATE && event.value <= MAX_TEMPO_RATE);
seen += 1;
}
}
while let Some(_event) = shared.pop_event() {
seen += 1;
}
let dropped = shared.dropped_events.load(Ordering::Relaxed);
assert_eq!(seen + dropped, 10_000);
}
}