//! Lock-free history of the input plus the host transport, shared by the audio and GUI threads. //! //! The audio thread writes samples with relaxed stores and publishes the frame count with a //! release store. The GUI thread reads the count with acquire, copies, then checks the count //! again: anything the writer may have overwritten during the copy is reported, never returned. use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; /// Transport markers kept for alignment; one is written at least every half second. pub const MARKERS: usize = 512; #[derive(Clone, Copy, Debug, PartialEq)] pub struct Transport { pub playing: bool, pub tempo: f64, /// Song position in quarter notes at the first sample of the block. pub beats: f64, pub numerator: u32, pub denominator: u32, } impl Transport { /// Bar length in quarter notes. pub fn bar(&self) -> f64 { self.numerator.max(1) as f64 * 4. / self.denominator.max(1) as f64 } } #[derive(Clone, Copy, Debug, PartialEq)] pub struct Marker { /// Absolute recorded frame index. pub frame: u64, pub transport: Transport, } impl Marker { /// Song position at `frame`, extrapolated at this marker's tempo. pub fn beats_at(&self, frame: f64, rate: f64) -> f64 { self.transport.beats + (frame - self.frame as f64) / rate * self.transport.tempo / 60. } pub fn frame_at(&self, beats: f64, rate: f64) -> f64 { self.frame as f64 + (beats - self.transport.beats) * 60. / self.transport.tempo * rate } } pub struct Ring { rate: f32, frames: usize, data: Box<[AtomicU32]>, written: AtomicU64, markers: Box<[[AtomicU64; 4]]>, marker_count: AtomicU64, peak: AtomicU32, } impl Ring { /// Stereo history of `seconds` at `rate`. pub fn new(rate: f32, seconds: f32) -> Self { let frames = (rate * seconds).ceil().max(1.) as usize; Self { rate, frames, data: (0..frames * 2).map(|_| AtomicU32::new(0)).collect(), written: AtomicU64::new(0), markers: (0..MARKERS) .map(|_| std::array::from_fn(|_| AtomicU64::new(0))) .collect(), marker_count: AtomicU64::new(0), peak: AtomicU32::new(0), } } pub fn rate(&self) -> f32 { self.rate } pub fn capacity(&self) -> usize { self.frames } pub fn written(&self) -> u64 { self.written.load(Ordering::Acquire) } /// Input peak since the last call, for the editor meter. pub fn take_peak(&self) -> f32 { f32::from_bits(self.peak.swap(0, Ordering::AcqRel)) } /// Markers that are certainly intact, oldest first. pub fn markers(&self) -> Vec { let count = self.marker_count.load(Ordering::Acquire); // A margin keeps slots the writer may be refilling during this read out of the result. let first = count.saturating_sub(MARKERS as u64 - 8); let mut out: Vec = (first..count) .map(|i| { let s = &self.markers[(i % MARKERS as u64) as usize]; let meta = s[3].load(Ordering::Relaxed); Marker { frame: s[0].load(Ordering::Relaxed), transport: Transport { beats: f64::from_bits(s[1].load(Ordering::Relaxed)), tempo: f64::from_bits(s[2].load(Ordering::Relaxed)), numerator: (meta & 0xffff) as u32, denominator: ((meta >> 16) & 0xffff) as u32, playing: meta >> 32 & 1 == 1, }, } }) .collect(); let after = self.marker_count.load(Ordering::Acquire); let safe = after.saturating_sub(MARKERS as u64 - 8); out.drain(..(safe.saturating_sub(first) as usize).min(out.len())); out } /// Copies frames `[start, end)` as interleaved stereo. Fails if any were overwritten. pub fn copy(&self, start: u64, end: u64) -> Result, &'static str> { let written = self.written(); if end > written || start > end { return Err("Capture range is not recorded yet."); } if written - start > self.frames as u64 { return Err("Capture is longer than the recording buffer."); } let mut out = Vec::with_capacity(((end - start) * 2) as usize); for f in start..end { let at = (f % self.frames as u64) as usize * 2; out.push(f32::from_bits(self.data[at].load(Ordering::Relaxed))); out.push(f32::from_bits(self.data[at + 1].load(Ordering::Relaxed))); } if self.written() - start > self.frames as u64 { return Err("Capture was overwritten while copying. Try again."); } Ok(out) } } /// Audio-thread writer. Never allocates. pub struct Recorder { last: Option, frame: u64, since_marker: u64, peak: f32, } impl Default for Recorder { fn default() -> Self { Self::new() } } impl Recorder { pub fn new() -> Self { Self { last: None, frame: 0, since_marker: 0, peak: 0., } } /// Call at the start of every block with the host transport. pub fn block(&mut self, ring: &Ring, transport: Option) { let Some(t) = transport.filter(|t| t.tempo.is_finite() && t.tempo > 1. && t.beats.is_finite()) else { return; }; let rate = ring.rate as f64; let changed = match self.last { None => true, Some(m) => { let expected = m.beats_at(self.frame as f64, rate); m.transport.playing != t.playing || (m.transport.tempo - t.tempo).abs() > 1e-6 || m.transport.numerator != t.numerator || m.transport.denominator != t.denominator || (t.playing && (expected - t.beats).abs() > 0.01) } }; if changed || self.since_marker as f64 >= rate * 0.5 { let m = Marker { frame: self.frame, transport: t, }; let count = ring.marker_count.load(Ordering::Relaxed); let s = &ring.markers[(count % MARKERS as u64) as usize]; s[0].store(m.frame, Ordering::Relaxed); s[1].store(t.beats.to_bits(), Ordering::Relaxed); s[2].store(t.tempo.to_bits(), Ordering::Relaxed); let meta = (t.numerator as u64 & 0xffff) | (t.denominator as u64 & 0xffff) << 16 | (t.playing as u64) << 32; s[3].store(meta, Ordering::Relaxed); ring.marker_count.store(count + 1, Ordering::Release); self.last = Some(m); self.since_marker = 0; } } #[inline] pub fn push(&mut self, ring: &Ring, frame: [f32; 2]) { let at = (self.frame % ring.frames as u64) as usize * 2; ring.data[at].store(frame[0].to_bits(), Ordering::Relaxed); ring.data[at + 1].store(frame[1].to_bits(), Ordering::Relaxed); self.frame += 1; self.since_marker += 1; self.peak = self.peak.max(frame[0].abs()).max(frame[1].abs()); } /// Makes the frames pushed so far visible to the GUI thread. Call at the end of a block. pub fn publish(&mut self, ring: &Ring) { ring.written.store(self.frame, Ordering::Release); let old = f32::from_bits(ring.peak.load(Ordering::Relaxed)); if self.peak > old { ring.peak.store(self.peak.to_bits(), Ordering::Relaxed); } self.peak = 0.; } }