Skip to main content

refrain_adapters/
schedule.rs

1//! Pattern → time-event schedule.
2//!
3//! Compiles a `Pattern` into a flat list of `Hap` (happening) events with
4//! absolute time stamps in cycles. Adapters consume the schedule and
5//! translate it to media-specific output (audio Hap JSON, OSC packets,
6//! visual draw calls, text streams, etc.).
7
8use refrain_core::{Op, Pattern};
9
10/// One scheduled happening: a value with a half-open time interval [start, end).
11#[derive(Debug, Clone, PartialEq)]
12pub struct Hap {
13    /// Cycle-relative start time.
14    pub start: f64,
15    /// Cycle-relative end time (start + duration).
16    pub end: f64,
17    /// Pitch name (e.g. "C4") or atom string.
18    pub pitch: Option<String>,
19    /// Free-form payload (kept stringly typed for v0.1; structured value in v0.2).
20    pub value: String,
21}
22
23impl Hap {
24    pub fn duration(&self) -> f64 {
25        self.end - self.start
26    }
27}
28
29/// Standard musical-symbol → cycle-fraction map.
30fn dur_to_cycles(d: &str) -> f64 {
31    match d {
32        "w" | "whole" => 1.0,
33        "h" | "half" => 0.5,
34        "q" | "quarter" => 0.25,
35        "e" | "eighth" => 0.125,
36        "s" | "sixteenth" => 0.0625,
37        // Numeric duration in cycles (e.g. "0.5") falls through to parse.
38        s => s.parse::<f64>().unwrap_or(0.25),
39    }
40}
41
42/// Compile a pattern starting at `t0`. Returns the list of Haps and the
43/// total duration consumed.
44pub fn schedule(pattern: &Pattern, t0: f64) -> (Vec<Hap>, f64) {
45    match pattern {
46        Pattern::Op(op) => schedule_op(op, t0),
47        Pattern::Seq(items) => {
48            let mut t = t0;
49            let mut out = Vec::with_capacity(items.len());
50            for p in items {
51                let (sub, dur) = schedule(p, t);
52                out.extend(sub);
53                t += dur;
54            }
55            (out, t - t0)
56        }
57    }
58}
59
60fn schedule_op(op: &Op, t0: f64) -> (Vec<Hap>, f64) {
61    match op {
62        Op::Note { pitch, dur } => {
63            let d = dur_to_cycles(dur);
64            (
65                vec![Hap {
66                    start: t0,
67                    end: t0 + d,
68                    pitch: Some(pitch.clone()),
69                    value: format!("note:{}:{}", pitch, dur),
70                }],
71                d,
72            )
73        }
74        Op::Loop { count, body } => {
75            let mut t = t0;
76            let mut out = Vec::new();
77            for _ in 0..*count {
78                let (sub, dur) = schedule(body, t);
79                out.extend(sub);
80                t += dur;
81            }
82            (out, t - t0)
83        }
84        Op::Diff { x, t } => (
85            vec![Hap {
86                start: t0,
87                end: t0,
88                pitch: None,
89                value: format!("diff:{}:{}", x, t),
90            }],
91            0.0,
92        ),
93        Op::Quotient { rels } => (
94            vec![Hap {
95                start: t0,
96                end: t0,
97                pitch: None,
98                value: format!("quotient:{}", rels.join(",")),
99            }],
100            0.0,
101        ),
102        Op::Sym(s) => (
103            vec![Hap {
104                start: t0,
105                end: t0,
106                pitch: None,
107                value: format!("sym:{}", s),
108            }],
109            0.0,
110        ),
111        Op::Call { head, args } => {
112            let mut out = Vec::new();
113            let mut t = t0;
114            for a in args {
115                let (sub, dur) = schedule(a, t);
116                out.extend(sub);
117                t += dur;
118            }
119            out.push(Hap {
120                start: t0,
121                end: t,
122                pitch: None,
123                value: format!("call:{}", head),
124            });
125            (out, t - t0)
126        }
127    }
128}