Skip to main content

treble_lang/ast/
program.rs

1//! Top-level program AST nodes (directives, pattern lines).
2
3use super::mini::MiniNotation;
4
5/// A complete parsed source file.
6#[derive(Debug, Clone, PartialEq)]
7pub struct Program {
8    pub lines: Vec<SourceLine>,
9}
10
11/// A single parsed line from the source.
12#[derive(Debug, Clone, PartialEq)]
13pub enum SourceLine {
14    /// `bpm <integer>`
15    Bpm(u32),
16    /// `sig <num>/<den>`
17    Sig(u8, u8),
18    /// `phrase <cycles>` — how many cycles a musical phrase spans.
19    Phrase(u32),
20    /// `scale <root> <mode>`
21    Scale(PitchRoot, ScaleMode),
22    /// `load "<path>"`
23    Load(String),
24    /// `arrange <name> [<name> …]` — the order the sections play in (§8.4).
25    Arrange(Vec<ArrangeItem>),
26    /// `tail <seconds>` — how long a render rings out past the last cycle (§8.7).
27    Tail(f64),
28    /// `seed <integer>` — salts the generative constructs (§8.8).
29    Seed(u64),
30    /// `meta <key> "<value>"` — a free-form tag travelling with the piece (§8.10).
31    Meta(String, String),
32    /// `include <instrument>` — explicitly make a registry instrument available.
33    Include(String),
34    /// A `def <name> { … }` instrument definition (§6).
35    ///
36    /// Boxed because a definition is several times the size of any other line,
37    /// and every `SourceLine` would otherwise pay for it.
38    Def(Box<super::instrument::InstrumentDef>),
39    /// A pattern line (possibly muted).
40    Pattern(PatternDef),
41    /// `[;] group <name> {` — opens an instrument group (§7). The member
42    /// pattern lines stay ordinary [`SourceLine::Pattern`]s so line-based
43    /// editing keeps working; [`parse_program`](crate::parser::parse_program)
44    /// tags each member's [`PatternDef::group`].
45    GroupStart { muted: bool, name: String },
46    /// `}` closing a group, optionally with `| transform` shared filters.
47    GroupEnd(Vec<Transform>),
48    /// `[;] section <name> <cycles> {` — opens a section (§8.2). Like a group,
49    /// the members stay ordinary [`SourceLine`]s so line-based editing keeps
50    /// working; [`parse_program`](crate::parser::parse_program) tags each
51    /// member's [`PatternDef::section`].
52    SectionStart {
53        muted: bool,
54        name: String,
55        cycles: u32,
56    },
57    /// `}` closing a section. Unlike a group's, it takes no pipeline — a
58    /// section is a span of time, not a bus.
59    SectionEnd,
60    /// A comment (kept for round-tripping, not evaluated).
61    Comment(String),
62    /// An empty/blank line.
63    Blank,
64}
65
66/// An instrument group (§7): member patterns share one bus filter chain.
67#[derive(Debug, Clone, PartialEq)]
68pub struct GroupDef {
69    /// A muted group silences every member without touching their own flags.
70    pub muted: bool,
71    pub name: String,
72    /// The shared chain after the `}`. The consumer restricts these to audio
73    /// transforms (plus `vel`, which distributes to the members).
74    pub transforms: Vec<Transform>,
75}
76
77/// A pattern definition:
78/// `[;] <name> <instrument> "<mini>" [@ span] [| transform ...]`
79#[derive(Debug, Clone, PartialEq)]
80pub struct PatternDef {
81    pub muted: bool,
82    pub name: String,
83    pub instrument: String,
84    pub notation: MiniNotation,
85    pub transforms: Vec<Transform>,
86    /// The group this line sits inside, if any — assigned by
87    /// [`parse_program`](crate::parser::parse_program), not by the line itself.
88    pub group: Option<String>,
89    /// The section this line sits inside, if any — likewise assigned by
90    /// [`parse_program`](crate::parser::parse_program). `None` in a live
91    /// buffer, and in a piece means a top-level line that sounds throughout.
92    pub section: Option<String>,
93    /// `@ 3..8` — where in its section the line sounds (§8.3). `None` is the
94    /// whole section. Only legal on a section member.
95    pub span: Option<Span>,
96}
97
98/// An instrument section (§8.2): a named block of a stated length in cycles.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct SectionDef {
101    /// A muted section still takes its time in the arrangement — it is a rest
102    /// of its own length, not a removal from it.
103    pub muted: bool,
104    pub name: String,
105    /// How many cycles the section lasts. Always >= 1.
106    pub cycles: u32,
107}
108
109/// One entry in an `arrange` line: a section and how many times in a row.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct ArrangeItem {
112    pub section: String,
113    /// `verse*2`. Always >= 1; a bare name is 1.
114    pub repeat: u32,
115}
116
117/// Where a line sounds inside its section — `@ 3..8`, `@ 5..`, `@ ..4`, `@ 8`.
118///
119/// Cycles are **1-based and inclusive at both ends**, matching how `a..b` reads
120/// everywhere else in the language and how a musician counts bars. Either end
121/// may be omitted, meaning the section's own start or end.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct Span {
124    pub from: Option<u32>,
125    pub to: Option<u32>,
126}
127
128impl Span {
129    /// A single cycle: `@ 8`.
130    pub fn at(cycle: u32) -> Self {
131        Self {
132            from: Some(cycle),
133            to: Some(cycle),
134        }
135    }
136
137    /// The first cycle the line sounds on, resolved against its section.
138    pub fn start(&self) -> u32 {
139        self.from.unwrap_or(1)
140    }
141
142    /// The last cycle the line sounds on, resolved against a section of
143    /// `cycles` length.
144    pub fn end(&self, cycles: u32) -> u32 {
145        self.to.unwrap_or(cycles)
146    }
147
148    /// Whether the line sounds on `cycle` (1-based) of a section of `cycles`
149    /// length.
150    pub fn contains(&self, cycle: u32, cycles: u32) -> bool {
151        cycle >= self.start() && cycle <= self.end(cycles)
152    }
153
154    /// Why this span could never sound in a section of `cycles` length, if so.
155    ///
156    /// A line that can never sound is a mistake rather than a silence, so the
157    /// two ways of writing one are reported rather than played as a rest.
158    pub fn conflict(&self, cycles: u32) -> Option<String> {
159        if let (Some(from), Some(to)) = (self.from, self.to)
160            && from > to
161        {
162            return Some(format!("span starts at cycle {from} but ends at {to}"));
163        }
164        if self.start() == 0 {
165            return Some("cycle 0: spans are 1-based".to_string());
166        }
167        let last = self.end(cycles);
168        if last > cycles {
169            return Some(format!(
170                "span reaches cycle {last}, past the section's {cycles}"
171            ));
172        }
173        if self.start() > cycles {
174            return Some(format!(
175                "span starts at cycle {}, past the section's {cycles}",
176                self.start()
177            ));
178        }
179        None
180    }
181}
182
183/// A pitch root (for scales/directives) — uppercase, with optional accidental.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub struct PitchRoot {
186    pub name: NoteLetter,
187    pub accidental: Accidental,
188}
189
190/// One of the seven note letters.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum NoteLetter {
193    C,
194    D,
195    E,
196    F,
197    G,
198    A,
199    B,
200}
201
202/// Accidental applied to a pitch.
203#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
204pub enum Accidental {
205    #[default]
206    Natural,
207    Sharp,
208    DoubleSharp,
209    Flat,
210    DoubleFlat,
211}
212
213/// Musical scale / mode.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum ScaleMode {
216    Major,
217    Minor,
218    Dorian,
219    Phrygian,
220    Lydian,
221    Mixolydian,
222    Aeolian,
223    Locrian,
224    Chromatic,
225    Pentatonic,
226    Blues,
227}
228
229/// A transform applied after the mini-notation via `|`.
230///
231/// Transforms split into two families, which matters to consumers: `Rev`,
232/// `Fast`, `Slow`, `Arp`, `Scale`, `Oct` and `Vel` reshape the scheduled
233/// events, while `Gain`, `Pan`, `Lpf`, `Hpf`, `Delay` and `Reverb` describe an
234/// ordered DSP chain.
235///
236/// Every numeric argument is a [`Ramp`], so any of them may travel across the
237/// line's [`Transform::RampSpan`] (§4.6). A parameter that only ever holds one
238/// value is a `Ramp::Fixed`, and [`Ramp::travels`] is how a consumer tells the
239/// two apart.
240#[derive(Debug, Clone, PartialEq)]
241pub enum Transform {
242    Rev,
243    Fast(Ramp<f64>),
244    Slow(Ramp<f64>),
245    Every(u32, Box<Transform>),
246    Arp(ArpMode),
247    Scale(PitchRoot, ScaleMode),
248    Oct(Ramp<i32>),
249    /// `vel <0.0..=1.0>` — the line's default note velocity. A step that names
250    /// its own velocity (`X`, `:v` — see [`crate::ast::mini::Step`]) overrides
251    /// this for that step.
252    Vel(Ramp<f64>),
253    /// `ramp <cycles> [lin|exp]` — how long the line's ranges take to travel,
254    /// and how they get there. One span governs every range on the line.
255    RampSpan {
256        cycles: u32,
257        curve: RampCurve,
258    },
259    /// `gain <0.0..=2.0>` — output level, positional in the DSP chain.
260    Gain(Ramp<f64>),
261    /// `pan <-1.0..=1.0>` — a fixed stereo position, positional in the chain.
262    Pan(Ramp<f64>),
263    /// `pan <wave> <rate>[hz] [depth]` — a stereo position swept by an LFO.
264    AutoPan(PanSweep),
265    Lpf(Ramp<f64>),
266    Hpf(Ramp<f64>),
267    /// `fx <filter> <arg>...`, or one of its short aliases.
268    ///
269    /// The filter is named but not resolved here: this crate has no knowledge
270    /// of the engine's filter set, so the consumer looks the name up in its own
271    /// registry and reports an unknown one.
272    Fx(FxCall),
273    /// `delay <time> <feedback> [mix]` — the wet mix defaults to 0.35.
274    Delay(Ramp<f64>, Ramp<f64>, Option<Ramp<f64>>),
275    Reverb(Ramp<f64>),
276}
277
278/// How a range gets from one end to the other across the `ramp` span.
279///
280/// The crate records the intent only. `Exp` asks for geometric interpolation —
281/// equal ratio steps rather than equal value steps, which is what a filter
282/// cutoff needs to sound like a steady opening — but resolving it is the
283/// consumer's job, including deciding what to do with a range that touches or
284/// crosses zero, where a geometric path does not exist.
285#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
286pub enum RampCurve {
287    /// `lin`, and the default when the curve is omitted, so buffers written
288    /// before curves existed keep their meaning.
289    #[default]
290    Linear,
291    /// `exp` — equal ratio steps.
292    Exponential,
293    /// `osc` — oscillate between the ends instead of arriving: a triangle
294    /// with the span as its period, `from → to → from`, wrapping forever.
295    Oscillate,
296}
297
298/// Arpeggiator mode.
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum ArpMode {
301    Up,
302    Down,
303    UpDown,
304    Random,
305}
306
307/// A swept stereo position: `pan sine 4`, `pan sq 1 0.6`, `pan tri 0.5hz`.
308#[derive(Debug, Clone, Copy, PartialEq)]
309pub struct PanSweep {
310    pub wave: LfoWave,
311    pub rate: LfoRate,
312    /// How far the sweep reaches, in `0.0..=1.0`. `None` means a full sweep.
313    pub depth: Option<f64>,
314}
315
316/// LFO shapes available to a swept parameter.
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub enum LfoWave {
319    Sine,
320    Triangle,
321    Square,
322    Saw,
323    Random,
324}
325
326/// How fast an LFO runs.
327///
328/// A bare number is musical — a period measured in cycles, which follows the
329/// tempo. An `hz` suffix opts into absolute time instead.
330#[derive(Debug, Clone, Copy, PartialEq)]
331pub enum LfoRate {
332    /// Period in cycles: `4` is one sweep every four cycles.
333    Cycles(f64),
334    /// Frequency in hertz: `0.5hz` is one sweep every two seconds.
335    Hertz(f64),
336}
337
338/// A call on an engine filter: the name the performer wrote, plus its arguments.
339#[derive(Debug, Clone, PartialEq)]
340pub struct FxCall {
341    /// Either a filter name or a short alias, exactly as written.
342    pub filter: String,
343    pub args: Vec<FxArg>,
344}
345
346/// One argument to an [`FxCall`].
347#[derive(Debug, Clone, PartialEq)]
348pub enum FxArg {
349    /// Fills the next of the filter's declared parameters.
350    Positional(FxValue),
351    /// Sets one parameter by name, leaving the rest at their defaults.
352    Named(String, FxValue),
353}
354
355/// An argument's value, which may travel across the line's `ramp` span.
356///
357/// The distinction is syntactic on purpose. Whether a bare number is a literal
358/// parameter value or a period in cycles depends on the parameter it lands on,
359/// which only the consumer knows. The legal interval is likewise the filter's
360/// own business, so nothing here range-checks the values.
361#[derive(Debug, Clone, PartialEq)]
362pub enum FxValue {
363    /// A bare number: `0.6`, `2..8`.
364    Plain(Ramp<f64>),
365    /// A number with an `hz` suffix, so always an absolute frequency.
366    Hertz(Ramp<f64>),
367}
368
369impl FxValue {
370    /// The ramp inside, whichever spelling it arrived in.
371    pub fn ramp(&self) -> &Ramp<f64> {
372        match self {
373            Self::Plain(ramp) | Self::Hertz(ramp) => ramp,
374        }
375    }
376
377    /// Whether this argument travels, and so needs the line to have a `ramp`.
378    pub fn travels(&self) -> bool {
379        self.ramp().travels()
380    }
381}
382
383/// A value that may travel over the line's [`Transform::RampSpan`].
384///
385/// Three shapes, because a build is not always a smooth sweep: `4` stays put,
386/// `4..16` sweeps continuously, and `2>4>8>16` holds each value in turn. All of
387/// them hold their final value once the span has passed — a crescendo arrives
388/// and stays until the performer changes it.
389#[derive(Debug, Clone, PartialEq)]
390pub enum Ramp<T> {
391    /// A plain value: `4`.
392    Fixed(T),
393    /// `4..16` — travels continuously from one end to the other.
394    Sweep { from: T, to: T },
395    /// `2>4>8>16` — each value held for an equal share of the span.
396    ///
397    /// Split into head and tail so the sequence can never be empty.
398    Steps { first: T, rest: Vec<T> },
399    /// `r(...)` — a travel that carries its own window instead of inheriting
400    /// the line's `| ramp`: a span in **time divisions** (the signature's
401    /// beats) and its own curve. In 4/4, `r(300,9000,4)` spans one cycle; in
402    /// 2/4 the same span is two. Written without internal spaces.
403    Timed {
404        /// The travel itself — always `Sweep` or `Steps`.
405        ramp: Box<Ramp<T>>,
406        /// Window length in time divisions. Fractions are legal: `0.5` is
407        /// half a beat, which with `osc` is a wobble.
408        span_divisions: f64,
409        curve: RampCurve,
410    },
411}
412
413impl<T: Copy> Ramp<T> {
414    pub fn fixed(value: T) -> Self {
415        Self::Fixed(value)
416    }
417
418    /// Build a stepped ramp, collapsing to a fixed value when nothing follows.
419    pub fn steps(first: T, rest: Vec<T>) -> Self {
420        if rest.is_empty() {
421            Self::Fixed(first)
422        } else {
423            Self::Steps { first, rest }
424        }
425    }
426
427    /// The value held before the ramp starts moving.
428    pub fn start(&self) -> T {
429        match self {
430            Self::Fixed(value) => *value,
431            Self::Sweep { from, .. } => *from,
432            Self::Steps { first, .. } => *first,
433            Self::Timed { ramp, .. } => ramp.start(),
434        }
435    }
436
437    /// Whether this value travels, and so needs a ramp span.
438    pub fn travels(&self) -> bool {
439        !matches!(self, Self::Fixed(_))
440    }
441
442    /// Every value the ramp passes through, for range checking.
443    pub fn values(&self) -> Vec<T> {
444        match self {
445            Self::Fixed(value) => vec![*value],
446            Self::Sweep { from, to } => vec![*from, *to],
447            Self::Steps { first, rest } => {
448                let mut all = vec![*first];
449                all.extend(rest.iter().copied());
450                all
451            }
452            Self::Timed { ramp, .. } => ramp.values(),
453        }
454    }
455
456    /// The window a `r(...)` value carries, if it has one of its own.
457    pub fn own_window(&self) -> Option<(f64, RampCurve)> {
458        match self {
459            Self::Timed {
460                span_divisions,
461                curve,
462                ..
463            } => Some((*span_divisions, *curve)),
464            _ => None,
465        }
466    }
467}