rill-core 0.5.0

Core foundation for the Rill ecosystem — traits, math, buffers, queues, time, macros
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! # Clock tick - the heartbeat of signal processing
//!
//! A `ClockTick` represents a single moment in signal time, containing
//! information about sample position, block size, and tempo.

use std::fmt;

/// A tick of the system clock
///
/// Sent to nodes on every signal block to provide timing information
/// and synchronize processing. This is the fundamental timing primitive
/// in Rill.
///
/// # Fields
///
/// * `sample_pos` - Absolute sample position since start
/// * `samples_since_last` - Number of samples since the last tick
/// * `is_new_block` - Whether this is the start of a new block
/// * `sample_rate` - Current sample rate in Hz
/// * `tempo` - Current tempo in BPM (if available)
/// * `source` - Which backend produced this tick (e.g. "alsa:default")
///
/// I/O access is handled directly through [`IoCapture`](crate::io::IoCapture)
/// and [`IoPlayback`](crate::io::IoPlayback) traits — the tick carries
/// only timing metadata.
///
/// # Example
///
/// ```
/// use rill_core::time::ClockTick;
///
/// let tick = ClockTick::new(44100, 64, 44100.0, "test".into());
/// assert_eq!(tick.absolute_seconds(), 1.0);
/// assert_eq!(tick.delta_seconds(), 64.0 / 44100.0);
/// ```
#[derive(Clone)]
pub struct ClockTick {
    /// Absolute sample position since start
    pub sample_pos: u64,

    /// Number of samples since the last tick
    pub samples_since_last: u32,

    /// Whether this is the start of a new block
    pub is_new_block: bool,

    /// Current sample rate in Hz
    pub sample_rate: f32,

    /// Current tempo in BPM (if available)
    pub tempo: Option<f32>,

    /// Which backend produced this tick (e.g. "alsa:default", "pipewire:0").
    pub source: String,

    /// Hardware clock correction factor: `configured_rate / actual_rate`.
    ///
    /// `1.0` = nominal (rates match). `< 1.0` = hardware runs faster.
    /// `> 1.0` = hardware runs slower.  Set by the backend when the
    /// negotiated hardware rate differs from the graph's configured rate.
    pub speed_ratio: f64,

    /// Whether this tick should trigger a ClockTick dispatch to modules.
    ///
    /// `true` by default.  Chunking backends (PipeWire) set this to `false`
    /// for intermediate chunks and `true` only for the final chunk of the
    /// DMA buffer — avoiding 48 ClockTick dispatches per PW callback.
    pub is_final: bool,

    /// Number of frames the I/O backend processes per callback (its quantum).
    ///
    /// The graph processes one `block_size` chunk per pass, but a backend may
    /// batch many chunks into a single I/O callback (e.g. PipeWire delivers a
    /// 12288-frame buffer = 48 × 256 chunks). Tick-driven control producers
    /// (sequencers/servos) that run asynchronously only see the results of a
    /// callback *after* it returns, so a change reacting to a tick in the
    /// current callback can only be rendered in the next one. Producers use
    /// `io_quantum` as the sample look-ahead when stamping
    /// [`SetParameter::sample_pos`](crate::queues::SetParameter) so the change
    /// lands on the correct block of the next callback instead of collapsing
    /// onto block 0. Defaults to `samples_since_last` (one chunk per callback).
    pub io_quantum: u32,
}

impl PartialEq for ClockTick {
    fn eq(&self, other: &Self) -> bool {
        self.sample_pos == other.sample_pos
            && self.samples_since_last == other.samples_since_last
            && self.is_new_block == other.is_new_block
            && self.sample_rate == other.sample_rate
            && self.tempo == other.tempo
            && self.source == other.source
    }
}

impl fmt::Debug for ClockTick {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ClockTick")
            .field("sample_pos", &self.sample_pos)
            .field("samples_since_last", &self.samples_since_last)
            .field("is_new_block", &self.is_new_block)
            .field("sample_rate", &self.sample_rate)
            .field("tempo", &self.tempo)
            .field("source", &self.source)
            .finish()
    }
}

impl ClockTick {
    /// Create a new clock tick
    ///
    /// # Arguments
    /// * `sample_pos` - Absolute sample position
    /// * `samples_since_last` - Samples since last tick
    /// * `sample_rate` - Sample rate in Hz
    /// * `source` - Backend source name
    ///
    /// # Returns
    /// A new `ClockTick` with `is_new_block = true` and `tempo = None`
    pub fn new(sample_pos: u64, samples_since_last: u32, sample_rate: f32, source: String) -> Self {
        Self {
            sample_pos,
            samples_since_last,
            is_new_block: true,
            sample_rate,
            tempo: None,
            source,
            speed_ratio: 1.0,
            is_final: true,
            io_quantum: samples_since_last,
        }
    }

    /// Create a new clock tick with tempo information
    ///
    /// # Arguments
    /// * `sample_pos` - Absolute sample position
    /// * `samples_since_last` - Samples since last tick
    /// * `sample_rate` - Sample rate in Hz
    /// * `tempo` - Tempo in BPM
    /// * `source` - Backend source name
    pub fn with_tempo(
        sample_pos: u64,
        samples_since_last: u32,
        sample_rate: f32,
        tempo: f32,
        source: String,
    ) -> Self {
        Self {
            sample_pos,
            samples_since_last,
            is_new_block: true,
            sample_rate,
            tempo: Some(tempo),
            source,
            speed_ratio: 1.0,
            is_final: true,
            io_quantum: samples_since_last,
        }
    }

    /// Set the I/O backend's per-callback quantum (frames processed per callback).
    ///
    /// Backends that batch multiple `block_size` chunks into one callback set
    /// this so asynchronous control producers know how far ahead to schedule
    /// sample-accurate parameter changes. See [`io_quantum`](Self::io_quantum).
    pub fn with_io_quantum(mut self, io_quantum: u32) -> Self {
        self.io_quantum = io_quantum;
        self
    }

    /// Get the time since the last tick in seconds
    ///
    /// # Returns
    /// Time in seconds since the previous tick
    #[inline(always)]
    pub fn delta_seconds(&self) -> f32 {
        self.samples_since_last as f32 / self.sample_rate
    }

    /// Get the absolute time in seconds since start
    ///
    /// # Returns
    /// Absolute time in seconds
    #[inline(always)]
    pub fn absolute_seconds(&self) -> f64 {
        self.sample_pos as f64 / self.sample_rate as f64
    }

    /// Get the current beat position (if tempo is available)
    ///
    /// # Returns
    /// * `Some(beat)` - Current beat position (fractional)
    /// * `None` - No tempo information available
    #[inline(always)]
    pub fn beat_position(&self) -> Option<f64> {
        self.tempo.map(|bpm| {
            let seconds_per_beat = 60.0 / bpm as f64;
            self.absolute_seconds() / seconds_per_beat
        })
    }

    /// Get the current bar-beat-sixteenth position (if tempo is available)
    ///
    /// # Returns
    /// * `Some((bar, beat, sixteenth))` - Musical position
    /// * `None` - No tempo information available
    pub fn musical_position(&self) -> Option<(u32, u8, u8)> {
        self.tempo.map(|bpm| {
            let seconds_per_beat = 60.0 / bpm as f64;
            let total_beats = self.absolute_seconds() / seconds_per_beat;

            let bar = (total_beats / 4.0).floor() as u32;
            let beat_in_bar = (total_beats % 4.0) as u8;
            let sixteenth = ((total_beats.fract() * 4.0) as u8) % 4;

            (bar, beat_in_bar, sixteenth)
        })
    }

    /// Advance to the next tick
    ///
    /// # Arguments
    /// * `samples` - Number of samples to advance
    pub fn advance(&mut self, samples: u32) {
        self.sample_pos += samples as u64;
        self.samples_since_last = samples;
        self.is_new_block = true;
    }

    /// Check if this tick is at the start of a new bar
    ///
    /// # Returns
    /// `true` if this is the first beat of a bar
    pub fn is_new_bar(&self) -> bool {
        if let Some((_, beat, sixteenth)) = self.musical_position() {
            beat == 0 && sixteenth == 0
        } else {
            false
        }
    }

    /// Check if this tick is at the start of a new beat
    ///
    /// # Returns
    /// `true` if this is the start of a beat
    pub fn is_new_beat(&self) -> bool {
        if let Some((_, _, sixteenth)) = self.musical_position() {
            sixteenth == 0
        } else {
            false
        }
    }
}

impl Default for ClockTick {
    fn default() -> Self {
        Self {
            sample_pos: 0,
            samples_since_last: 0,
            is_new_block: false,
            sample_rate: 44100.0,
            tempo: None,
            source: String::new(),
            speed_ratio: 1.0,
            is_final: true,
            io_quantum: 0,
        }
    }
}

impl fmt::Display for ClockTick {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "ClockTick(pos={}, delta={}ms, rate={}Hz, source={}",
            self.sample_pos,
            self.delta_seconds() * 1000.0,
            self.sample_rate,
            self.source,
        )?;

        if let Some(tempo) = self.tempo {
            write!(f, ", tempo={}BPM", tempo)?;
        }

        write!(f, ")")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_clock_tick_creation() {
        let tick = ClockTick::new(44100, 44100, 44100.0, "test".into());
        assert_eq!(tick.sample_pos, 44100);
        assert_eq!(tick.samples_since_last, 44100);
        assert!(tick.is_new_block);
        assert_eq!(tick.sample_rate, 44100.0);
        assert_eq!(tick.tempo, None);
        assert_eq!(tick.source, "test");
    }

    #[test]
    fn test_clock_tick_with_tempo() {
        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0, "test".into());
        assert_eq!(tick.tempo, Some(120.0));
    }

    #[test]
    fn test_delta_seconds() {
        let tick = ClockTick::new(0, 44100, 44100.0, "test".into());
        assert_eq!(tick.delta_seconds(), 1.0);

        let tick = ClockTick::new(0, 22050, 44100.0, "test".into());
        assert_eq!(tick.delta_seconds(), 0.5);
    }

    #[test]
    fn test_absolute_seconds() {
        let tick = ClockTick::new(44100, 44100, 44100.0, "test".into());
        assert_eq!(tick.absolute_seconds(), 1.0);

        let tick = ClockTick::new(88200, 44100, 44100.0, "test".into());
        assert_eq!(tick.absolute_seconds(), 2.0);
    }

    #[test]
    fn test_beat_position() {
        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0, "test".into());
        // At 120 BPM, one beat = 0.5 seconds
        // 1 second = 2 beats
        assert_eq!(tick.beat_position(), Some(2.0));
    }

    #[test]
    fn test_musical_position() {
        let tick = ClockTick::with_tempo(44100 * 2, 44100, 44100.0, 120.0, "test".into());
        // 2 seconds at 120 BPM = 4 beats
        // 4 beats = 1 bar
        let pos = tick.musical_position();
        assert_eq!(pos, Some((1, 0, 0)));

        let tick = ClockTick::with_tempo(44100 * 3, 44100, 44100.0, 120.0, "test".into());
        // 3 seconds = 6 beats = 1.5 bars
        let pos = tick.musical_position();
        assert_eq!(pos, Some((1, 2, 0)));
    }

    #[test]
    fn test_advance() {
        let mut tick = ClockTick::new(0, 0, 44100.0, "test".into());
        tick.advance(64);
        assert_eq!(tick.sample_pos, 64);
        assert_eq!(tick.samples_since_last, 64);
        assert!(tick.is_new_block);
    }

    #[test]
    fn test_is_new_bar() {
        let tick = ClockTick::with_tempo(0, 0, 44100.0, 120.0, "test".into());
        assert!(tick.is_new_bar());

        let tick = ClockTick::with_tempo(22050, 22050, 44100.0, 120.0, "test".into());
        // 0.5 seconds = 1 beat, not new bar
        assert!(!tick.is_new_bar());
    }

    #[test]
    fn test_is_new_beat() {
        let tick = ClockTick::with_tempo(0, 0, 44100.0, 120.0, "test".into());
        assert!(tick.is_new_beat());

        let tick = ClockTick::with_tempo(11025, 11025, 44100.0, 120.0, "test".into());
        // 0.25 seconds = half beat, not new beat
        assert!(!tick.is_new_beat());
    }

    #[test]
    fn test_default() {
        let tick = ClockTick::default();
        assert_eq!(tick.sample_pos, 0);
        assert_eq!(tick.samples_since_last, 0);
        assert!(!tick.is_new_block);
        assert_eq!(tick.sample_rate, 44100.0);
        assert_eq!(tick.tempo, None);
        assert_eq!(tick.source, "");
    }

    #[test]
    fn test_display() {
        let tick = ClockTick::new(44100, 44100, 44100.0, "test".into());
        let display = format!("{}", tick);
        assert!(display.contains("pos=44100"));
        assert!(display.contains("delta=1000ms"));
        assert!(display.contains("source=test"));

        let tick = ClockTick::with_tempo(44100, 44100, 44100.0, 120.0, "test".into());
        let display = format!("{}", tick);
        assert!(display.contains("tempo=120BPM"));
    }
}