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}