Skip to main content

rill_core/time/
tick.rs

1//! # Clock tick - the heartbeat of signal processing
2//!
3//! A `ClockTick` represents a single moment in signal time, containing
4//! information about sample position, block size, and tempo.
5
6use std::fmt;
7
8/// A tick of the system clock
9///
10/// Sent to nodes on every signal block to provide timing information
11/// and synchronize processing. This is the fundamental timing primitive
12/// in Rill.
13///
14/// # Fields
15///
16/// * `sample_pos` - Absolute sample position since start
17/// * `samples_since_last` - Number of samples since the last tick
18/// * `is_new_block` - Whether this is the start of a new block
19/// * `sample_rate` - Current sample rate in Hz
20/// * `tempo` - Current tempo in BPM (if available)
21/// * `source` - Which backend produced this tick (e.g. "alsa:default")
22///
23/// I/O access is handled directly through [`IoCapture`](crate::io::IoCapture)
24/// and [`IoPlayback`](crate::io::IoPlayback) traits — the tick carries
25/// only timing metadata.
26///
27/// # Example
28///
29/// ```
30/// use rill_core::time::ClockTick;
31///
32/// let tick = ClockTick::new(44100, 64, 44100.0, "test".into());
33/// assert_eq!(tick.absolute_seconds(), 1.0);
34/// assert_eq!(tick.delta_seconds(), 64.0 / 44100.0);
35/// ```
36#[derive(Clone)]
37pub struct ClockTick {
38    /// Absolute sample position since start
39    pub sample_pos: u64,
40
41    /// Number of samples since the last tick
42    pub samples_since_last: u32,
43
44    /// Whether this is the start of a new block
45    pub is_new_block: bool,
46
47    /// Current sample rate in Hz
48    pub sample_rate: f32,
49
50    /// Current tempo in BPM (if available)
51    pub tempo: Option<f32>,
52
53    /// Which backend produced this tick (e.g. "alsa:default", "pipewire:0").
54    pub source: String,
55
56    /// Hardware clock correction factor: `configured_rate / actual_rate`.
57    ///
58    /// `1.0` = nominal (rates match). `< 1.0` = hardware runs faster.
59    /// `> 1.0` = hardware runs slower.  Set by the backend when the
60    /// negotiated hardware rate differs from the graph's configured rate.
61    pub speed_ratio: f64,
62
63    /// Whether this tick should trigger a ClockTick dispatch to modules.
64    ///
65    /// `true` by default.  Chunking backends (PipeWire) set this to `false`
66    /// for intermediate chunks and `true` only for the final chunk of the
67    /// DMA buffer — avoiding 48 ClockTick dispatches per PW callback.
68    pub is_final: bool,
69}
70
71impl PartialEq for ClockTick {
72    fn eq(&self, other: &Self) -> bool {
73        self.sample_pos == other.sample_pos
74            && self.samples_since_last == other.samples_since_last
75            && self.is_new_block == other.is_new_block
76            && self.sample_rate == other.sample_rate
77            && self.tempo == other.tempo
78            && self.source == other.source
79    }
80}
81
82impl fmt::Debug for ClockTick {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.debug_struct("ClockTick")
85            .field("sample_pos", &self.sample_pos)
86            .field("samples_since_last", &self.samples_since_last)
87            .field("is_new_block", &self.is_new_block)
88            .field("sample_rate", &self.sample_rate)
89            .field("tempo", &self.tempo)
90            .field("source", &self.source)
91            .finish()
92    }
93}
94
95impl ClockTick {
96    /// Create a new clock tick
97    ///
98    /// # Arguments
99    /// * `sample_pos` - Absolute sample position
100    /// * `samples_since_last` - Samples since last tick
101    /// * `sample_rate` - Sample rate in Hz
102    /// * `source` - Backend source name
103    ///
104    /// # Returns
105    /// A new `ClockTick` with `is_new_block = true` and `tempo = None`
106    pub fn new(sample_pos: u64, samples_since_last: u32, sample_rate: f32, source: String) -> Self {
107        Self {
108            sample_pos,
109            samples_since_last,
110            is_new_block: true,
111            sample_rate,
112            tempo: None,
113            source,
114            speed_ratio: 1.0,
115            is_final: true,
116        }
117    }
118
119    /// Create a new clock tick with tempo information
120    ///
121    /// # Arguments
122    /// * `sample_pos` - Absolute sample position
123    /// * `samples_since_last` - Samples since last tick
124    /// * `sample_rate` - Sample rate in Hz
125    /// * `tempo` - Tempo in BPM
126    /// * `source` - Backend source name
127    pub fn with_tempo(
128        sample_pos: u64,
129        samples_since_last: u32,
130        sample_rate: f32,
131        tempo: f32,
132        source: String,
133    ) -> Self {
134        Self {
135            sample_pos,
136            samples_since_last,
137            is_new_block: true,
138            sample_rate,
139            tempo: Some(tempo),
140            source,
141            speed_ratio: 1.0,
142            is_final: true,
143        }
144    }
145
146    /// Get the time since the last tick in seconds
147    ///
148    /// # Returns
149    /// Time in seconds since the previous tick
150    #[inline(always)]
151    pub fn delta_seconds(&self) -> f32 {
152        self.samples_since_last as f32 / self.sample_rate
153    }
154
155    /// Get the absolute time in seconds since start
156    ///
157    /// # Returns
158    /// Absolute time in seconds
159    #[inline(always)]
160    pub fn absolute_seconds(&self) -> f64 {
161        self.sample_pos as f64 / self.sample_rate as f64
162    }
163
164    /// Get the current beat position (if tempo is available)
165    ///
166    /// # Returns
167    /// * `Some(beat)` - Current beat position (fractional)
168    /// * `None` - No tempo information available
169    #[inline(always)]
170    pub fn beat_position(&self) -> Option<f64> {
171        self.tempo.map(|bpm| {
172            let seconds_per_beat = 60.0 / bpm as f64;
173            self.absolute_seconds() / seconds_per_beat
174        })
175    }
176
177    /// Get the current bar-beat-sixteenth position (if tempo is available)
178    ///
179    /// # Returns
180    /// * `Some((bar, beat, sixteenth))` - Musical position
181    /// * `None` - No tempo information available
182    pub fn musical_position(&self) -> Option<(u32, u8, u8)> {
183        self.tempo.map(|bpm| {
184            let seconds_per_beat = 60.0 / bpm as f64;
185            let total_beats = self.absolute_seconds() / seconds_per_beat;
186
187            let bar = (total_beats / 4.0).floor() as u32;
188            let beat_in_bar = (total_beats % 4.0) as u8;
189            let sixteenth = ((total_beats.fract() * 4.0) as u8) % 4;
190
191            (bar, beat_in_bar, sixteenth)
192        })
193    }
194
195    /// Advance to the next tick
196    ///
197    /// # Arguments
198    /// * `samples` - Number of samples to advance
199    pub fn advance(&mut self, samples: u32) {
200        self.sample_pos += samples as u64;
201        self.samples_since_last = samples;
202        self.is_new_block = true;
203    }
204
205    /// Check if this tick is at the start of a new bar
206    ///
207    /// # Returns
208    /// `true` if this is the first beat of a bar
209    pub fn is_new_bar(&self) -> bool {
210        if let Some((_, beat, sixteenth)) = self.musical_position() {
211            beat == 0 && sixteenth == 0
212        } else {
213            false
214        }
215    }
216
217    /// Check if this tick is at the start of a new beat
218    ///
219    /// # Returns
220    /// `true` if this is the start of a beat
221    pub fn is_new_beat(&self) -> bool {
222        if let Some((_, _, sixteenth)) = self.musical_position() {
223            sixteenth == 0
224        } else {
225            false
226        }
227    }
228}
229
230impl Default for ClockTick {
231    fn default() -> Self {
232        Self {
233            sample_pos: 0,
234            samples_since_last: 0,
235            is_new_block: false,
236            sample_rate: 44100.0,
237            tempo: None,
238            source: String::new(),
239            speed_ratio: 1.0,
240            is_final: true,
241        }
242    }
243}
244
245impl fmt::Display for ClockTick {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(
248            f,
249            "ClockTick(pos={}, delta={}ms, rate={}Hz, source={}",
250            self.sample_pos,
251            self.delta_seconds() * 1000.0,
252            self.sample_rate,
253            self.source,
254        )?;
255
256        if let Some(tempo) = self.tempo {
257            write!(f, ", tempo={}BPM", tempo)?;
258        }
259
260        write!(f, ")")
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_clock_tick_creation() {
270        let tick = ClockTick::new(44100, 44100, 44100.0, "test".into());
271        assert_eq!(tick.sample_pos, 44100);
272        assert_eq!(tick.samples_since_last, 44100);
273        assert!(tick.is_new_block);
274        assert_eq!(tick.sample_rate, 44100.0);
275        assert_eq!(tick.tempo, None);
276        assert_eq!(tick.source, "test");
277    }
278
279    #[test]
280    fn test_clock_tick_with_tempo() {
281        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0, "test".into());
282        assert_eq!(tick.tempo, Some(120.0));
283    }
284
285    #[test]
286    fn test_delta_seconds() {
287        let tick = ClockTick::new(0, 44100, 44100.0, "test".into());
288        assert_eq!(tick.delta_seconds(), 1.0);
289
290        let tick = ClockTick::new(0, 22050, 44100.0, "test".into());
291        assert_eq!(tick.delta_seconds(), 0.5);
292    }
293
294    #[test]
295    fn test_absolute_seconds() {
296        let tick = ClockTick::new(44100, 44100, 44100.0, "test".into());
297        assert_eq!(tick.absolute_seconds(), 1.0);
298
299        let tick = ClockTick::new(88200, 44100, 44100.0, "test".into());
300        assert_eq!(tick.absolute_seconds(), 2.0);
301    }
302
303    #[test]
304    fn test_beat_position() {
305        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0, "test".into());
306        // At 120 BPM, one beat = 0.5 seconds
307        // 1 second = 2 beats
308        assert_eq!(tick.beat_position(), Some(2.0));
309    }
310
311    #[test]
312    fn test_musical_position() {
313        let tick = ClockTick::with_tempo(44100 * 2, 44100, 44100.0, 120.0, "test".into());
314        // 2 seconds at 120 BPM = 4 beats
315        // 4 beats = 1 bar
316        let pos = tick.musical_position();
317        assert_eq!(pos, Some((1, 0, 0)));
318
319        let tick = ClockTick::with_tempo(44100 * 3, 44100, 44100.0, 120.0, "test".into());
320        // 3 seconds = 6 beats = 1.5 bars
321        let pos = tick.musical_position();
322        assert_eq!(pos, Some((1, 2, 0)));
323    }
324
325    #[test]
326    fn test_advance() {
327        let mut tick = ClockTick::new(0, 0, 44100.0, "test".into());
328        tick.advance(64);
329        assert_eq!(tick.sample_pos, 64);
330        assert_eq!(tick.samples_since_last, 64);
331        assert!(tick.is_new_block);
332    }
333
334    #[test]
335    fn test_is_new_bar() {
336        let tick = ClockTick::with_tempo(0, 0, 44100.0, 120.0, "test".into());
337        assert!(tick.is_new_bar());
338
339        let tick = ClockTick::with_tempo(22050, 22050, 44100.0, 120.0, "test".into());
340        // 0.5 seconds = 1 beat, not new bar
341        assert!(!tick.is_new_bar());
342    }
343
344    #[test]
345    fn test_is_new_beat() {
346        let tick = ClockTick::with_tempo(0, 0, 44100.0, 120.0, "test".into());
347        assert!(tick.is_new_beat());
348
349        let tick = ClockTick::with_tempo(11025, 11025, 44100.0, 120.0, "test".into());
350        // 0.25 seconds = half beat, not new beat
351        assert!(!tick.is_new_beat());
352    }
353
354    #[test]
355    fn test_default() {
356        let tick = ClockTick::default();
357        assert_eq!(tick.sample_pos, 0);
358        assert_eq!(tick.samples_since_last, 0);
359        assert!(!tick.is_new_block);
360        assert_eq!(tick.sample_rate, 44100.0);
361        assert_eq!(tick.tempo, None);
362        assert_eq!(tick.source, "");
363    }
364
365    #[test]
366    fn test_display() {
367        let tick = ClockTick::new(44100, 44100, 44100.0, "test".into());
368        let display = format!("{}", tick);
369        assert!(display.contains("pos=44100"));
370        assert!(display.contains("delta=1000ms"));
371        assert!(display.contains("source=test"));
372
373        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0, "test".into());
374        let display = format!("{}", tick);
375        assert!(display.contains("tempo=120BPM"));
376    }
377}