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 audio time, containing
4//! information about sample position, block size, and tempo.
5
6use std::fmt;
7
8/// A tick of the audio 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///
22/// # Example
23///
24/// ```
25/// use rill_core::time::ClockTick;
26///
27/// let tick = ClockTick::new(44100, 64, 44100.0);
28/// assert_eq!(tick.absolute_seconds(), 1.0);
29/// assert_eq!(tick.delta_seconds(), 64.0 / 44100.0);
30/// ```
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct ClockTick {
33    /// Absolute sample position since start
34    pub sample_pos: u64,
35
36    /// Number of samples since the last tick
37    pub samples_since_last: u32,
38
39    /// Whether this is the start of a new block
40    pub is_new_block: bool,
41
42    /// Current sample rate in Hz
43    pub sample_rate: f32,
44
45    /// Current tempo in BPM (if available)
46    pub tempo: Option<f32>,
47}
48
49impl ClockTick {
50    /// Create a new clock tick
51    ///
52    /// # Arguments
53    /// * `sample_pos` - Absolute sample position
54    /// * `samples_since_last` - Samples since last tick
55    /// * `sample_rate` - Sample rate in Hz
56    ///
57    /// # Returns
58    /// A new `ClockTick` with `is_new_block = true` and `tempo = None`
59    pub const fn new(sample_pos: u64, samples_since_last: u32, sample_rate: f32) -> Self {
60        Self {
61            sample_pos,
62            samples_since_last,
63            is_new_block: true,
64            sample_rate,
65            tempo: None,
66        }
67    }
68
69    /// Create a new clock tick with tempo information
70    ///
71    /// # Arguments
72    /// * `sample_pos` - Absolute sample position
73    /// * `samples_since_last` - Samples since last tick
74    /// * `sample_rate` - Sample rate in Hz
75    /// * `tempo` - Tempo in BPM
76    pub const fn with_tempo(
77        sample_pos: u64,
78        samples_since_last: u32,
79        sample_rate: f32,
80        tempo: f32,
81    ) -> Self {
82        Self {
83            sample_pos,
84            samples_since_last,
85            is_new_block: true,
86            sample_rate,
87            tempo: Some(tempo),
88        }
89    }
90
91    /// Get the time since the last tick in seconds
92    ///
93    /// # Returns
94    /// Time in seconds since the previous tick
95    #[inline(always)]
96    pub fn delta_seconds(&self) -> f32 {
97        self.samples_since_last as f32 / self.sample_rate
98    }
99
100    /// Get the absolute time in seconds since start
101    ///
102    /// # Returns
103    /// Absolute time in seconds
104    #[inline(always)]
105    pub fn absolute_seconds(&self) -> f64 {
106        self.sample_pos as f64 / self.sample_rate as f64
107    }
108
109    /// Get the current beat position (if tempo is available)
110    ///
111    /// # Returns
112    /// * `Some(beat)` - Current beat position (fractional)
113    /// * `None` - No tempo information available
114    #[inline(always)]
115    pub fn beat_position(&self) -> Option<f64> {
116        self.tempo.map(|bpm| {
117            let seconds_per_beat = 60.0 / bpm as f64;
118            self.absolute_seconds() / seconds_per_beat
119        })
120    }
121
122    /// Get the current bar-beat-sixteenth position (if tempo is available)
123    ///
124    /// # Returns
125    /// * `Some((bar, beat, sixteenth))` - Musical position
126    /// * `None` - No tempo information available
127    pub fn musical_position(&self) -> Option<(u32, u8, u8)> {
128        self.tempo.map(|bpm| {
129            let seconds_per_beat = 60.0 / bpm as f64;
130            let total_beats = self.absolute_seconds() / seconds_per_beat;
131
132            let bar = (total_beats / 4.0).floor() as u32;
133            let beat_in_bar = (total_beats % 4.0) as u8;
134            let sixteenth = ((total_beats.fract() * 4.0) as u8) % 4;
135
136            (bar, beat_in_bar, sixteenth)
137        })
138    }
139
140    /// Advance to the next tick
141    ///
142    /// # Arguments
143    /// * `samples` - Number of samples to advance
144    pub fn advance(&mut self, samples: u32) {
145        self.sample_pos += samples as u64;
146        self.samples_since_last = samples;
147        self.is_new_block = true;
148    }
149
150    /// Check if this tick is at the start of a new bar
151    ///
152    /// # Returns
153    /// `true` if this is the first beat of a bar
154    pub fn is_new_bar(&self) -> bool {
155        if let Some((_, beat, sixteenth)) = self.musical_position() {
156            beat == 0 && sixteenth == 0
157        } else {
158            false
159        }
160    }
161
162    /// Check if this tick is at the start of a new beat
163    ///
164    /// # Returns
165    /// `true` if this is the start of a beat
166    pub fn is_new_beat(&self) -> bool {
167        if let Some((_, _, sixteenth)) = self.musical_position() {
168            sixteenth == 0
169        } else {
170            false
171        }
172    }
173}
174
175impl Default for ClockTick {
176    fn default() -> Self {
177        Self {
178            sample_pos: 0,
179            samples_since_last: 0,
180            is_new_block: false,
181            sample_rate: 44100.0,
182            tempo: None,
183        }
184    }
185}
186
187impl fmt::Display for ClockTick {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(
190            f,
191            "ClockTick(pos={}, delta={}ms, rate={}Hz",
192            self.sample_pos,
193            self.delta_seconds() * 1000.0,
194            self.sample_rate,
195        )?;
196
197        if let Some(tempo) = self.tempo {
198            write!(f, ", tempo={}BPM", tempo)?;
199        }
200
201        write!(f, ")")
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_clock_tick_creation() {
211        let tick = ClockTick::new(44100, 44100, 44100.0);
212        assert_eq!(tick.sample_pos, 44100);
213        assert_eq!(tick.samples_since_last, 44100);
214        assert!(tick.is_new_block);
215        assert_eq!(tick.sample_rate, 44100.0);
216        assert_eq!(tick.tempo, None);
217    }
218
219    #[test]
220    fn test_clock_tick_with_tempo() {
221        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0);
222        assert_eq!(tick.tempo, Some(120.0));
223    }
224
225    #[test]
226    fn test_delta_seconds() {
227        let tick = ClockTick::new(0, 44100, 44100.0);
228        assert_eq!(tick.delta_seconds(), 1.0);
229
230        let tick = ClockTick::new(0, 22050, 44100.0);
231        assert_eq!(tick.delta_seconds(), 0.5);
232    }
233
234    #[test]
235    fn test_absolute_seconds() {
236        let tick = ClockTick::new(44100, 44100, 44100.0);
237        assert_eq!(tick.absolute_seconds(), 1.0);
238
239        let tick = ClockTick::new(88200, 44100, 44100.0);
240        assert_eq!(tick.absolute_seconds(), 2.0);
241    }
242
243    #[test]
244    fn test_beat_position() {
245        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0);
246        // At 120 BPM, one beat = 0.5 seconds
247        // 1 second = 2 beats
248        assert_eq!(tick.beat_position(), Some(2.0));
249    }
250
251    #[test]
252    fn test_musical_position() {
253        let tick = ClockTick::with_tempo(44100 * 2, 44100, 44100.0, 120.0);
254        // 2 seconds at 120 BPM = 4 beats
255        // 4 beats = 1 bar
256        let pos = tick.musical_position();
257        assert_eq!(pos, Some((1, 0, 0)));
258
259        let tick = ClockTick::with_tempo(44100 * 3, 44100, 44100.0, 120.0);
260        // 3 seconds = 6 beats = 1.5 bars
261        let pos = tick.musical_position();
262        assert_eq!(pos, Some((1, 2, 0)));
263    }
264
265    #[test]
266    fn test_advance() {
267        let mut tick = ClockTick::new(0, 0, 44100.0);
268        tick.advance(64);
269        assert_eq!(tick.sample_pos, 64);
270        assert_eq!(tick.samples_since_last, 64);
271        assert!(tick.is_new_block);
272    }
273
274    #[test]
275    fn test_is_new_bar() {
276        let tick = ClockTick::with_tempo(0, 0, 44100.0, 120.0);
277        assert!(tick.is_new_bar());
278
279        let tick = ClockTick::with_tempo(22050, 22050, 44100.0, 120.0);
280        // 0.5 seconds = 1 beat, not new bar
281        assert!(!tick.is_new_bar());
282    }
283
284    #[test]
285    fn test_is_new_beat() {
286        let tick = ClockTick::with_tempo(0, 0, 44100.0, 120.0);
287        assert!(tick.is_new_beat());
288
289        let tick = ClockTick::with_tempo(11025, 11025, 44100.0, 120.0);
290        // 0.25 seconds = half beat, not new beat
291        assert!(!tick.is_new_beat());
292    }
293
294    #[test]
295    fn test_default() {
296        let tick = ClockTick::default();
297        assert_eq!(tick.sample_pos, 0);
298        assert_eq!(tick.samples_since_last, 0);
299        assert!(!tick.is_new_block);
300        assert_eq!(tick.sample_rate, 44100.0);
301        assert_eq!(tick.tempo, None);
302    }
303
304    #[test]
305    fn test_display() {
306        let tick = ClockTick::new(44100, 44100, 44100.0);
307        let display = format!("{}", tick);
308        assert!(display.contains("pos=44100"));
309        assert!(display.contains("delta=1000ms"));
310
311        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0);
312        let display = format!("{}", tick);
313        assert!(display.contains("tempo=120BPM"));
314    }
315}