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}