Skip to main content

rill_core/time/
render.rs

1use std::fmt;
2
3/// Musical transport state — derived from JACK/PipeWire transport or MIDI clock.
4///
5/// Carries the current bar/beat/tempo state of the host timeline.
6/// Populated by the I/O backend once per processing block.
7#[derive(Debug, Clone, Copy)]
8pub struct TransportState {
9    /// Whether the host transport is currently rolling (playing).
10    pub is_playing: bool,
11    /// Current tempo in beats per minute.
12    pub bpm: f64,
13    /// Absolute transport frame (song position in samples).
14    pub frame_pos: u64,
15    /// Beats per bar — time signature numerator (default 4).
16    pub time_sig_num: u8,
17    /// Beat unit — time signature denominator (default 4 = quarter note).
18    pub time_sig_den: u8,
19    /// Sample position where the current bar started.
20    pub bar_start_frame: u64,
21}
22
23impl Default for TransportState {
24    fn default() -> Self {
25        Self {
26            is_playing: true,
27            bpm: 120.0,
28            frame_pos: 0,
29            time_sig_num: 4,
30            time_sig_den: 4,
31            bar_start_frame: 0,
32        }
33    }
34}
35
36impl fmt::Display for TransportState {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(
39            f,
40            "{} {:.1} BPM {} {}/{} frame={} bar_start={}",
41            if self.is_playing { "▶" } else { "⏹" },
42            self.bpm,
43            if self.is_playing {
44                "rolling"
45            } else {
46                "stopped"
47            },
48            self.time_sig_num,
49            self.time_sig_den,
50            self.frame_pos,
51            self.bar_start_frame,
52        )
53    }
54}
55
56/// Unified render context — passed by `&` reference through the entire
57/// signal graph during one processing block.
58///
59/// Built by the I/O backend once per block on the stack. Contains sample-clock
60/// metadata, host transport state, and hardware clock correction factor.
61///
62/// # Field summary
63///
64/// | Field | Source | Used by |
65/// |-------|--------|---------|
66/// | `sample_pos` | I/O backend sample counter | Time-aware generators, sequencers |
67/// | `samples_since_last` | Block size | Delta-time computation |
68/// | `sample_rate` | I/O backend config | Frequency calculations, filters |
69/// | `transport` | JACK/PipeWire transport or MIDI clock | BPM-synced LFOs, sequencers, automations |
70/// | `speed_ratio` | SPA clock / DLL filter | Sample-rate conversion (resampling compensation) |
71#[derive(Debug, Clone)]
72pub struct RenderContext {
73    /// Absolute sample position since graph start.
74    pub sample_pos: u64,
75    /// Number of samples processed in this block.
76    pub samples_since_last: u32,
77    /// Current sample rate in Hz.
78    pub sample_rate: f32,
79    /// Host transport state (BPM, playing flag, musical position).
80    pub transport: TransportState,
81    /// Hardware clock correction factor.
82    ///
83    /// `1.0` = nominal. `> 1.0` = sound card is slow (resample up).
84    /// `< 1.0` = sound card is fast (resample down).
85    /// Set by PipeWire's `spa_io_clock.rate_match` or a JACK DLL filter.
86    pub speed_ratio: f64,
87}
88
89impl RenderContext {
90    /// Create a minimal render context from basic clock parameters.
91    ///
92    /// Transport defaults to 120 BPM, playing, 4/4.
93    pub fn new(sample_pos: u64, samples_since_last: u32, sample_rate: f32) -> Self {
94        Self {
95            sample_pos,
96            samples_since_last,
97            sample_rate,
98            transport: TransportState::default(),
99            speed_ratio: 1.0,
100        }
101    }
102
103    /// Create a render context with an explicit BPM.
104    pub fn with_tempo(
105        sample_pos: u64,
106        samples_since_last: u32,
107        sample_rate: f32,
108        bpm: f32,
109    ) -> Self {
110        Self {
111            sample_pos,
112            samples_since_last,
113            sample_rate,
114            transport: TransportState {
115                bpm: bpm as f64,
116                ..TransportState::default()
117            },
118            speed_ratio: 1.0,
119        }
120    }
121
122    /// Time delta of this block in seconds.
123    pub fn delta_seconds(&self) -> f64 {
124        self.samples_since_last as f64 / self.sample_rate as f64
125    }
126
127    /// Absolute time in seconds since graph start.
128    pub fn absolute_seconds(&self) -> f64 {
129        self.sample_pos as f64 / self.sample_rate as f64
130    }
131
132    /// Current BPM as `f32` for backward compatibility.
133    pub fn bpm(&self) -> f32 {
134        self.transport.bpm as f32
135    }
136
137    /// Musical beat position (requires valid BPM).
138    ///
139    /// Returns `None` if BPM is zero.
140    pub fn beat_position(&self) -> Option<f64> {
141        if self.transport.bpm <= 0.0 {
142            return None;
143        }
144        let seconds_per_beat = 60.0 / self.transport.bpm;
145        Some(self.absolute_seconds() / seconds_per_beat)
146    }
147
148    /// Musical position as `(bar, beat_in_bar, sixteenth)` triple.
149    ///
150    /// Uses `time_sig_num` and `time_sig_den` from the transport state.
151    pub fn musical_position(&self) -> Option<(u32, u8, u8)> {
152        self.beat_position().map(|total_beats| {
153            let beats_per_bar = self.transport.time_sig_num as f64;
154            let bar = (total_beats / beats_per_bar).floor() as u32;
155            let beat_in_bar = (total_beats % beats_per_bar) as u8;
156            let sixteenth = ((total_beats.fract() * 4.0) as u8) % 4;
157            (bar, beat_in_bar, sixteenth)
158        })
159    }
160
161    /// Whether this block starts a new bar.
162    pub fn is_new_bar(&self) -> bool {
163        self.musical_position()
164            .map(|(_bar, beat, sixteenth)| beat == 0 && sixteenth == 0)
165            .unwrap_or(false)
166    }
167
168    /// Whether this block starts a new beat.
169    pub fn is_new_beat(&self) -> bool {
170        self.musical_position()
171            .map(|(_bar, _beat, sixteenth)| sixteenth == 0)
172            .unwrap_or(false)
173    }
174}
175
176impl fmt::Display for RenderContext {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(
179            f,
180            "RenderContext(sample={}, block={}, sr={:.0}, transport=[{}], ratio={:.6})",
181            self.sample_pos,
182            self.samples_since_last,
183            self.sample_rate,
184            self.transport,
185            self.speed_ratio,
186        )
187    }
188}