Skip to main content

rill_patchbay/sequencer/
engine.rs

1use std::collections::HashMap;
2
3use super::pattern::{Pattern, StepPlayMode};
4use super::snapshot::Snapshot;
5use rill_core::queues::{SetParameter, SignalSource};
6use rill_core::traits::{ParameterId, PortId};
7
8/// The core sequencer state machine.
9///
10/// Driven by incoming CLOCK_TICK telemetry from the audio thread.  Call
11/// [`tick`](Self::tick) or [`tick_ext`](Self::tick_ext) every time a clock
12/// sample-position arrives; the sequencer checks whether the current step's
13/// duration has elapsed and advances if so, returning the p-lock parameter
14/// commands for the new step.
15///
16/// # Thread safety
17///
18/// `SnapshotSequencer` is `Send` but **not** `Sync`.  It should live inside
19/// a single task (typically the tokio task that drains the telemetry
20/// receiver).  External control (start/stop/pattern change) uses a command
21/// channel (see [`SequencerHandle`]).
22#[derive(Debug, Clone)]
23pub struct SnapshotSequencer {
24    /// Named snapshots for quick recall.
25    snapshots: HashMap<String, Snapshot>,
26    /// Registered patterns.
27    patterns: HashMap<String, Pattern>,
28    /// Currently active pattern ID.
29    active_pattern: String,
30    /// Index of the current step within the active pattern.
31    current_step: usize,
32    /// Absolute sample position when the current step started.
33    step_start_sample: u64,
34    /// Play direction for PingPong mode (1 = forward, -1 = backward).
35    direction: i8,
36    /// Whether the sequencer is running.
37    running: bool,
38    /// Latest beat position received from telemetry.
39    latest_beat_position: f32,
40    /// Whether the latest tick was a new beat boundary.
41    latest_new_beat: bool,
42    /// Whether the latest tick was a new bar boundary.
43    latest_new_bar: bool,
44}
45
46impl SnapshotSequencer {
47    /// Create a new, empty sequencer (no patterns, not running).
48    pub fn new() -> Self {
49        Self {
50            snapshots: HashMap::new(),
51            patterns: HashMap::new(),
52            active_pattern: String::new(),
53            current_step: 0,
54            step_start_sample: 0,
55            direction: 1,
56            running: false,
57            latest_beat_position: 0.0,
58            latest_new_beat: false,
59            latest_new_bar: false,
60        }
61    }
62
63    /// Create a sequencer pre-loaded with snapshots and patterns.
64    ///
65    /// The first pattern in the vec becomes the active pattern.
66    pub fn with_lib(snapshots: Vec<Snapshot>, patterns: Vec<Pattern>) -> Self {
67        let mut s = Self::new();
68        for snap in snapshots {
69            s.add_snapshot(snap);
70        }
71        for pat in patterns {
72            s.add_pattern(pat);
73        }
74        if let Some(first) = s.patterns.keys().next() {
75            s.active_pattern = first.clone();
76        }
77        s
78    }
79
80    // ── Snapshots ────────────────────────────────────────────────────
81
82    /// Register or replace a named snapshot.
83    pub fn add_snapshot(&mut self, snapshot: Snapshot) {
84        self.snapshots.insert(snapshot.id.clone(), snapshot);
85    }
86
87    /// Get a snapshot by ID, if present.
88    pub fn get_snapshot(&self, id: &str) -> Option<&Snapshot> {
89        self.snapshots.get(id)
90    }
91
92    /// Remove a snapshot by ID.  Does *not* affect existing patterns.
93    pub fn remove_snapshot(&mut self, id: &str) -> bool {
94        self.snapshots.remove(id).is_some()
95    }
96
97    // ── Patterns ─────────────────────────────────────────────────────
98
99    /// Register or replace a named pattern.
100    pub fn add_pattern(&mut self, pattern: Pattern) {
101        if self.active_pattern.is_empty() {
102            self.active_pattern = pattern.id.clone();
103        }
104        self.patterns.insert(pattern.id.clone(), pattern);
105    }
106
107    /// Get a pattern by ID, if present.
108    pub fn get_pattern(&self, id: &str) -> Option<&Pattern> {
109        self.patterns.get(id)
110    }
111
112    /// Remove a pattern by ID.  If it is the active pattern the sequencer
113    /// stops.
114    pub fn remove_pattern(&mut self, id: &str) -> bool {
115        if self.patterns.remove(id).is_some() {
116            if self.active_pattern == id {
117                self.active_pattern.clear();
118                self.running = false;
119            }
120            true
121        } else {
122            false
123        }
124    }
125
126    /// Switch to a different pattern (may be empty).
127    ///
128    /// Resets the step counter to 0.  If the pattern does not exist the
129    /// call is ignored.
130    pub fn set_active_pattern(&mut self, id: &str) {
131        if self.patterns.contains_key(id) || id.is_empty() {
132            self.active_pattern = id.to_string();
133            self.current_step = 0;
134            self.step_start_sample = 0;
135            self.direction = 1;
136        }
137    }
138
139    /// Active pattern ID.
140    pub fn active_pattern(&self) -> &str {
141        &self.active_pattern
142    }
143
144    // ── Transport ────────────────────────────────────────────────────
145
146    /// Start or resume the sequencer from the current step.
147    pub fn start(&mut self) {
148        self.running = true;
149    }
150
151    /// Pause the sequencer (keeps current step position).
152    pub fn stop(&mut self) {
153        self.running = false;
154    }
155
156    /// Reset the sequencer to step 0 at the given sample position.
157    pub fn reset(&mut self, sample_pos: u64) {
158        self.current_step = 0;
159        self.step_start_sample = sample_pos;
160        self.direction = 1;
161    }
162
163    /// Whether the sequencer is running.
164    pub fn is_running(&self) -> bool {
165        self.running
166    }
167
168    /// Index of the current step within the active pattern.
169    pub fn current_step(&self) -> usize {
170        self.current_step
171    }
172
173    /// Latest beat position received via [`tick_ext`](Self::tick_ext).
174    ///
175    /// Updated on every clock tick; `0.0` if no tempo data or
176    /// [`tick`](Self::tick) is used.
177    pub fn latest_beat_position(&self) -> f32 {
178        self.latest_beat_position
179    }
180
181    /// Whether the latest clock tick was at a beat boundary.
182    pub fn is_new_beat(&self) -> bool {
183        self.latest_new_beat
184    }
185
186    /// Whether the latest clock tick was at a bar boundary.
187    pub fn is_new_bar(&self) -> bool {
188        self.latest_new_bar
189    }
190
191    // ── Tick (the main state-machine entry point) ────────────────────
192
193    /// Advance the sequencer by one clock tick (basic version).
194    ///
195    /// Convenience wrapper around [`tick_ext`](Self::tick_ext) that passes
196    /// default beat info (beat_position=0, no beat/bar boundaries).
197    /// Prefer `tick_ext` when beat-aware CLOCK_TICK telemetry is available.
198    pub fn tick(&mut self, sample_pos: u64, sample_rate: f32, tempo: f32) -> Vec<SetParameter> {
199        self.tick_ext(sample_pos, sample_rate, tempo, 0.0, false, false)
200    }
201
202    /// Advance the sequencer by one clock tick with beat-aware telemetry.
203    ///
204    /// Call this from the telemetry listener task every time a `CLOCK_TICK`
205    /// event arrives from the audio thread.  The extended parameters
206    /// (`beat_position`, `is_new_beat`, `is_new_bar`) are stored in the
207    /// sequencer state and can be queried by algorithmic sequencer logic
208    /// (see [`latest_beat_position`](Self::latest_beat_position),
209    /// [`is_new_beat`](Self::is_new_beat),
210    /// [`is_new_bar`](Self::is_new_bar)).
211    ///
212    /// Returns a batch of [`SetParameter`] values to push when a step
213    /// boundary is crossed, or an empty `Vec` if no step change occurred.
214    pub fn tick_ext(
215        &mut self,
216        sample_pos: u64,
217        sample_rate: f32,
218        tempo: f32,
219        beat_position: f32,
220        is_new_beat: bool,
221        is_new_bar: bool,
222    ) -> Vec<SetParameter> {
223        self.latest_beat_position = beat_position;
224        self.latest_new_beat = is_new_beat;
225        self.latest_new_bar = is_new_bar;
226        if !self.running {
227            return Vec::new();
228        }
229
230        let (len, play_mode, step_dur) = {
231            let pat = match self.patterns.get(&self.active_pattern) {
232                Some(p) if !p.steps.is_empty() => p,
233                _ => return Vec::new(),
234            };
235            let step = &pat.steps[self.current_step];
236            (
237                pat.steps.len(),
238                pat.play_mode,
239                step.duration_samples(tempo, sample_rate),
240            )
241        };
242
243        let elapsed = sample_pos.saturating_sub(self.step_start_sample);
244
245        if elapsed >= step_dur {
246            self.current_step = self.advance_step(len, play_mode);
247            self.step_start_sample = sample_pos;
248
249            if let Some(pat) = self.patterns.get(&self.active_pattern) {
250                if self.current_step < pat.steps.len() {
251                    let new_step = &pat.steps[self.current_step];
252                    return new_step
253                        .parameters
254                        .iter()
255                        .map(|p| SetParameter::new(
256                            PortId::param(p.node_id, 0),
257                            ParameterId::new(&p.param_name).unwrap(),
258                            p.value,
259                            SignalSource::Manual,
260                        ))
261                        .collect();
262                }
263            }
264        }
265
266        Vec::new()
267    }
268
269    /// Pick the next step index and update direction for PingPong mode.
270    fn advance_step(&mut self, len: usize, play_mode: StepPlayMode) -> usize {
271        if len == 0 {
272            return 0;
273        }
274        match play_mode {
275            StepPlayMode::OneShot => (self.current_step + 1).min(len.saturating_sub(1)),
276            StepPlayMode::Loop => (self.current_step + 1) % len,
277            StepPlayMode::PingPong => {
278                let next = self.current_step as isize + self.direction as isize;
279                if next < 0 {
280                    self.direction = 1;
281                    1
282                } else if next >= len as isize {
283                    self.direction = -1;
284                    len.saturating_sub(2)
285                } else {
286                    next as usize
287                }
288            }
289            StepPlayMode::Random => {
290                use rand::Rng;
291                let mut rng = rand::thread_rng();
292                rng.gen_range(0..len)
293            }
294            StepPlayMode::Brownian => {
295                use rand::Rng;
296                let mut rng = rand::thread_rng();
297                let offset: isize = rng.gen_range(-1..=1);
298                (self.current_step as isize + offset).clamp(0, len.saturating_sub(1) as isize)
299                    as usize
300            }
301        }
302    }
303}
304
305impl Default for SnapshotSequencer {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311// =============================================================================
312// Sequencer command channel & handle
313// =============================================================================
314
315/// Commands sent to a running sequencer from another thread.
316#[derive(Debug, Clone, PartialEq)]
317pub enum SequencerCommand {
318    /// Start playback.
319    Start,
320    /// Stop playback.
321    Stop,
322    /// Reset to the given sample position.
323    Reset {
324        /// Target sample position for the reset.
325        sample_pos: u64,
326    },
327    /// Switch to a named pattern.
328    SetPattern(String),
329}
330
331/// Handle for controlling a [`SnapshotSequencer`] that lives inside a
332/// tokio task.
333///
334/// Clone the handle to share control of the sequencer across multiple
335/// threads (e.g. multiple OSC handlers).
336#[derive(Debug, Clone)]
337pub struct SequencerHandle {
338    cmd_tx: std::sync::Arc<crossbeam_channel::Sender<SequencerCommand>>,
339}
340
341impl SequencerHandle {
342    pub(crate) fn new(cmd_tx: crossbeam_channel::Sender<SequencerCommand>) -> Self {
343        Self {
344            cmd_tx: std::sync::Arc::new(cmd_tx),
345        }
346    }
347
348    /// Start the sequencer.
349    pub fn start(&self) {
350        let _ = self.cmd_tx.try_send(SequencerCommand::Start);
351    }
352
353    /// Stop the sequencer.
354    pub fn stop(&self) {
355        let _ = self.cmd_tx.try_send(SequencerCommand::Stop);
356    }
357
358    /// Reset the sequencer to the given sample position.
359    pub fn reset(&self, sample_pos: u64) {
360        let _ = self.cmd_tx.try_send(SequencerCommand::Reset { sample_pos });
361    }
362
363    /// Switch to a different pattern by ID.
364    pub fn set_pattern(&self, id: &str) {
365        let _ = self
366            .cmd_tx
367            .try_send(SequencerCommand::SetPattern(id.to_string()));
368    }
369}
370
371// =============================================================================
372// Tests
373// =============================================================================
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::sequencer::{ParameterTarget, SequenceStep};
379    use rill_core::NodeId;
380
381    fn make_step(value: f32, dur: f64) -> SequenceStep {
382        SequenceStep::single(NodeId(1), "param", value, dur)
383    }
384
385    fn simple_pattern() -> Pattern {
386        Pattern::new(
387            "p1",
388            vec![
389                make_step(0.0, 1.0),
390                make_step(0.5, 1.0),
391                make_step(1.0, 1.0),
392                make_step(0.5, 1.0),
393            ],
394        )
395    }
396
397    #[test]
398    fn test_sequencer_loop() {
399        let mut seq = SnapshotSequencer::with_lib(vec![], vec![simple_pattern()]);
400        seq.start();
401
402        let sr = 48000.0;
403        let tempo = 120.0;
404
405        let cmds = seq.tick(24000, sr, tempo);
406        assert!(!cmds.is_empty(), "should advance to step 1");
407        assert_eq!(seq.current_step, 1);
408
409        let cmds = seq.tick(48000, sr, tempo);
410        assert!(!cmds.is_empty());
411        assert_eq!(seq.current_step, 2);
412
413        let cmds = seq.tick(72000, sr, tempo);
414        assert!(!cmds.is_empty());
415        assert_eq!(seq.current_step, 3);
416
417        let cmds = seq.tick(96000, sr, tempo);
418        assert!(!cmds.is_empty());
419        assert_eq!(seq.current_step, 0);
420    }
421
422    #[test]
423    fn test_sequencer_not_running() {
424        let mut seq = SnapshotSequencer::with_lib(vec![], vec![simple_pattern()]);
425        let cmds = seq.tick(24000, 48000.0, 120.0);
426        assert!(cmds.is_empty());
427        assert_eq!(seq.current_step, 0);
428    }
429
430    #[test]
431    fn test_sequencer_stop() {
432        let mut seq = SnapshotSequencer::with_lib(vec![], vec![simple_pattern()]);
433        seq.start();
434        seq.tick(24000, 48000.0, 120.0);
435        assert_eq!(seq.current_step, 1);
436
437        seq.stop();
438        seq.tick(48000, 48000.0, 120.0);
439        assert_eq!(seq.current_step, 1, "should not advance after stop");
440    }
441
442    #[test]
443    fn test_sequencer_pingpong() {
444        let mut seq = SnapshotSequencer::with_lib(
445            vec![],
446            vec![Pattern::new(
447                "p1",
448                vec![
449                    make_step(0.0, 1.0),
450                    make_step(0.5, 1.0),
451                    make_step(1.0, 1.0),
452                ],
453            )
454            .with_mode(StepPlayMode::PingPong)],
455        );
456        seq.start();
457
458        seq.tick(24000, 48000.0, 120.0);
459        assert_eq!(seq.current_step, 1);
460        seq.tick(48000, 48000.0, 120.0);
461        assert_eq!(seq.current_step, 2);
462        seq.tick(72000, 48000.0, 120.0);
463        assert_eq!(seq.current_step, 1);
464        seq.tick(96000, 48000.0, 120.0);
465        assert_eq!(seq.current_step, 0);
466    }
467
468    #[test]
469    fn test_sequencer_oneshot() {
470        let mut seq = SnapshotSequencer::with_lib(
471            vec![],
472            vec![
473                Pattern::new("p1", vec![make_step(0.0, 1.0), make_step(0.5, 1.0)])
474                    .with_mode(StepPlayMode::OneShot),
475            ],
476        );
477        seq.start();
478
479        seq.tick(24000, 48000.0, 120.0);
480        assert_eq!(seq.current_step, 1);
481        seq.tick(48000, 48000.0, 120.0);
482        assert_eq!(seq.current_step, 1);
483    }
484
485    #[test]
486    fn test_sequencer_set_pattern() {
487        let mut seq = SnapshotSequencer::with_lib(
488            vec![],
489            vec![
490                Pattern::new("a", vec![make_step(1.0, 1.0)]),
491                Pattern::new("b", vec![make_step(0.0, 1.0), make_step(0.5, 1.0)]),
492            ],
493        );
494        seq.set_active_pattern("a");
495        seq.start();
496
497        assert_eq!(seq.active_pattern(), "a");
498
499        seq.set_active_pattern("b");
500        assert_eq!(seq.active_pattern(), "b");
501        assert_eq!(seq.current_step, 0);
502
503        let cmds = seq.tick(24000, 48000.0, 120.0);
504        assert!(!cmds.is_empty());
505        assert_eq!(seq.current_step, 1);
506
507        let cmds = seq.tick(48000, 48000.0, 120.0);
508        assert!(!cmds.is_empty());
509        assert_eq!(seq.current_step, 0);
510    }
511
512    #[test]
513    fn test_step_duration_samples() {
514        let step = make_step(0.5, 1.0);
515        assert_eq!(step.duration_samples(120.0, 48000.0), 24000);
516        assert_eq!(step.duration_samples(120.0, 44100.0), 22050);
517        assert_eq!(step.duration_samples(60.0, 48000.0), 48000);
518
519        let eighth = make_step(0.5, 0.5);
520        assert_eq!(eighth.duration_samples(120.0, 48000.0), 12000);
521
522        let sixteenth = make_step(0.5, 0.25);
523        assert_eq!(sixteenth.duration_samples(120.0, 48000.0), 6000);
524    }
525
526    #[test]
527    fn test_parameter_target_creation() {
528        let pt = ParameterTarget::new(NodeId(1), "gain", 0.5);
529        assert_eq!(pt.node_id, NodeId(1));
530        assert_eq!(pt.param_name, "gain");
531        assert_eq!(pt.value, 0.5);
532    }
533
534    #[test]
535    fn test_sequencer_handle_send() {
536        let (tx, rx) = crossbeam_channel::unbounded::<SequencerCommand>();
537        let handle = SequencerHandle::new(tx);
538
539        handle.start();
540        assert_eq!(rx.try_recv(), Ok(SequencerCommand::Start));
541
542        handle.stop();
543        assert_eq!(rx.try_recv(), Ok(SequencerCommand::Stop));
544
545        handle.set_pattern("foo");
546        assert_eq!(
547            rx.try_recv(),
548            Ok(SequencerCommand::SetPattern("foo".into()))
549        );
550
551        handle.reset(12345);
552        assert_eq!(
553            rx.try_recv(),
554            Ok(SequencerCommand::Reset { sample_pos: 12345 })
555        );
556    }
557
558    #[test]
559    fn test_tick_ext_stores_beat_info() {
560        let mut seq = SnapshotSequencer::new();
561        let pat = Pattern::new("p1", vec![SequenceStep::single(NodeId(1), "p", 0.5, 1.0)]);
562        seq.add_pattern(pat);
563        seq.set_active_pattern("p1");
564        seq.start();
565
566        let _ = seq.tick_ext(0, 48000.0, 120.0, 0.0, true, true);
567        assert!((seq.latest_beat_position() - 0.0).abs() < 1e-6);
568        assert!(seq.is_new_beat());
569        assert!(seq.is_new_bar());
570
571        let _ = seq.tick(24000, 48000.0, 120.0);
572        assert!((seq.latest_beat_position() - 0.0).abs() < 1e-6);
573        assert!(!seq.is_new_beat());
574        assert!(!seq.is_new_bar());
575
576        let _ = seq.tick_ext(48000, 48000.0, 120.0, 2.5, false, false);
577        assert!((seq.latest_beat_position() - 2.5).abs() < 1e-6);
578        assert!(!seq.is_new_beat());
579        assert!(!seq.is_new_bar());
580    }
581}