Skip to main content

treble_lang/piece/
mod.rs

1//! Piece resolution (§8).
2//!
3//! A [`Piece`] is what a buffer means once its `section` blocks have been put
4//! in the order its `arrange` line names: a flat timeline of section
5//! occurrences with an absolute starting cycle, and therefore a definite
6//! length. That is the whole difference between a piece and a live buffer —
7//! a piece can be compiled up front and rendered to a file, where a live
8//! buffer only ever knows what is sounding now.
9//!
10//! This module resolves and validates; it does not schedule. Turning the
11//! timeline into audio is the consumer's half, exactly as it is for the live
12//! language.
13
14use crate::ast::{
15    ArrangeItem, GroupDef, PatternDef, PitchRoot, Program, ScaleMode, SourceLine, Transform,
16};
17use crate::error::{CompileError, CompileErrorKind, SourceLocation};
18
19/// One resolved section: its members and the state in force while it plays.
20///
21/// Everything a section sounds like lives here rather than on the
22/// [`Occurrence`], because a section sounds the same wherever the arrangement
23/// puts it (§8.5). The only thing an occurrence adds is *when*.
24#[derive(Debug, Clone, PartialEq)]
25pub struct Section {
26    pub name: String,
27    /// A muted section still takes its time — it is a rest of its own length.
28    pub muted: bool,
29    /// How many cycles it lasts. Always >= 1.
30    pub cycles: u32,
31    /// Tempo in force, after any `bpm` written inside the section (§8.5).
32    pub bpm: u32,
33    /// Time signature in force.
34    pub sig: (u8, u8),
35    /// Default scale in force.
36    pub scale: Option<(PitchRoot, ScaleMode)>,
37    /// Member pattern lines, in source order.
38    pub patterns: Vec<PatternDef>,
39    /// Groups declared inside the section, in source order.
40    pub groups: Vec<GroupDef>,
41}
42
43impl Section {
44    /// How long one of this section's cycles lasts, in seconds.
45    pub fn cycle_seconds(&self) -> f64 {
46        cycle_seconds(self.bpm, self.sig)
47    }
48
49    /// How long the whole section lasts, in seconds.
50    pub fn seconds(&self) -> f64 {
51        self.cycle_seconds() * f64::from(self.cycles)
52    }
53
54    /// The members audible on `cycle` (1-based, within this section), skipping
55    /// the muted ones and those whose span (§8.3) excludes the cycle.
56    pub fn audible_on(&self, cycle: u32) -> impl Iterator<Item = &PatternDef> {
57        let cycles = self.cycles;
58        let muted = self.muted;
59        self.patterns.iter().filter(move |pattern| {
60            !muted && !pattern.muted && pattern.span.is_none_or(|span| span.contains(cycle, cycles))
61        })
62    }
63}
64
65/// One playing of a section: which one, and where it lands in the piece.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct Occurrence {
68    /// Index into [`Piece::sections`].
69    pub section: usize,
70    /// The absolute cycle the occurrence starts on, counted from 0 at the
71    /// start of the piece.
72    pub start_cycle: u64,
73}
74
75/// A buffer resolved into a timeline (§8).
76#[derive(Debug, Clone, PartialEq)]
77pub struct Piece {
78    /// Every section, in source order.
79    pub sections: Vec<Section>,
80    /// The arrangement as written, or the sections in source order when the
81    /// buffer has no `arrange` line (§8.4).
82    pub arrangement: Vec<ArrangeItem>,
83    /// The flat timeline: one entry per section occurrence, in playing order.
84    pub timeline: Vec<Occurrence>,
85    /// Lines outside every section. They sound for the whole piece — a drone,
86    /// a click track — and carry no span.
87    pub throughout: Vec<PatternDef>,
88    /// How long a render rings out past the last cycle, in seconds (§8.7).
89    pub tail: f64,
90    /// Salt for the generative constructs (§8.8).
91    pub seed: u64,
92    /// Sections the arrangement never plays. Legal — a sketch is not an error
93    /// — but worth reporting, so it is surfaced rather than swallowed.
94    pub unused: Vec<String>,
95    /// `meta` tags in source order (§8.10). Free-form: the language does not
96    /// enumerate the keys, it only carries them.
97    pub metadata: Vec<(String, String)>,
98}
99
100/// The default `tail` when a piece does not set one (§8.7).
101pub const DEFAULT_TAIL: f64 = 2.0;
102
103impl Piece {
104    /// How many cycles the arrangement lasts.
105    pub fn total_cycles(&self) -> u64 {
106        self.timeline
107            .iter()
108            .map(|occurrence| u64::from(self.sections[occurrence.section].cycles))
109            .sum()
110    }
111
112    /// How long the arrangement lasts in seconds, excluding [`Piece::tail`].
113    ///
114    /// Summed per occurrence rather than per cycle because a `sig` or `bpm`
115    /// scoped to a section changes how long its cycles are (§8.5).
116    pub fn seconds(&self) -> f64 {
117        self.timeline
118            .iter()
119            .map(|occurrence| self.sections[occurrence.section].seconds())
120            .sum()
121    }
122
123    /// The value of a `meta` tag, if the piece sets one.
124    pub fn meta(&self, key: &str) -> Option<&str> {
125        self.metadata
126            .iter()
127            .find(|(name, _)| name == key)
128            .map(|(_, value)| value.as_str())
129    }
130
131    /// How long a render lasts: the arrangement plus its tail.
132    pub fn render_seconds(&self) -> f64 {
133        self.seconds() + self.tail
134    }
135
136    /// Walk the timeline, yielding each occurrence with its section and the
137    /// second it starts at.
138    ///
139    /// The start times are accumulated here rather than stored, so a `bpm`
140    /// edit inside one section cannot leave a stale offset behind on another.
141    pub fn walk(&self) -> impl Iterator<Item = PlacedSection<'_>> {
142        let mut at_seconds = 0.0;
143        self.timeline.iter().map(move |occurrence| {
144            let section = &self.sections[occurrence.section];
145            let placed = PlacedSection {
146                section,
147                start_cycle: occurrence.start_cycle,
148                start_seconds: at_seconds,
149            };
150            at_seconds += section.seconds();
151            placed
152        })
153    }
154}
155
156/// One occurrence, resolved to its section and its position in real time.
157#[derive(Debug, Clone, Copy, PartialEq)]
158pub struct PlacedSection<'a> {
159    pub section: &'a Section,
160    /// Absolute cycle the occurrence starts on, counted from 0.
161    pub start_cycle: u64,
162    /// Absolute time the occurrence starts at, in seconds.
163    pub start_seconds: f64,
164}
165
166/// How long one cycle lasts at `bpm` in `signature`, in seconds.
167pub fn cycle_seconds(bpm: u32, signature: (u8, u8)) -> f64 {
168    let quarters = f64::from(signature.0) * 4.0 / f64::from(signature.1.max(1));
169    quarters * 60.0 / f64::from(bpm.max(1))
170}
171
172/// Whether a program is a piece: it has at least one `section` (§8.1).
173pub fn is_piece(program: &Program) -> bool {
174    program
175        .lines
176        .iter()
177        .any(|line| matches!(line, SourceLine::SectionStart { .. }))
178}
179
180fn error(line: usize, message: String) -> CompileError {
181    CompileError {
182        kind: CompileErrorKind::ParseError,
183        location: SourceLocation {
184            line,
185            column: 1,
186            file: None,
187        },
188        message,
189        suggestion: None,
190    }
191}
192
193/// State carried down the buffer while resolving, so a section can inherit the
194/// values in force above it and restore them when it ends (§8.5).
195#[derive(Clone, Copy)]
196struct Ambient {
197    bpm: u32,
198    sig: (u8, u8),
199    scale: Option<(PitchRoot, ScaleMode)>,
200}
201
202/// Resolve a parsed program into a [`Piece`].
203///
204/// `defaults` is the state a buffer starts in — the session's own `bpm`, `sig`
205/// and `scale` — which top-level directives then override.
206///
207/// Returns the piece alongside any errors. A piece is returned even when the
208/// errors are non-empty so a consumer can still show the structure it did
209/// understand; whether to play it is the consumer's call.
210pub fn resolve(
211    program: &Program,
212    defaults: (u32, (u8, u8), Option<(PitchRoot, ScaleMode)>),
213) -> (Piece, Vec<CompileError>) {
214    let mut errors = Vec::new();
215    let mut ambient = Ambient {
216        bpm: defaults.0,
217        sig: defaults.1,
218        scale: defaults.2,
219    };
220
221    let mut sections: Vec<Section> = Vec::new();
222    let mut throughout: Vec<PatternDef> = Vec::new();
223    let mut arrangement: Option<Vec<ArrangeItem>> = None;
224    let mut tail = DEFAULT_TAIL;
225    let mut seed = 0u64;
226    let mut metadata: Vec<(String, String)> = Vec::new();
227
228    // The section being filled, and the ambient state to restore when it ends.
229    let mut open: Option<(Section, Ambient)> = None;
230    // The group being filled inside the current section.
231    let mut open_group: Option<(bool, String, usize)> = None;
232
233    for (index, line) in program.lines.iter().enumerate() {
234        let at = index + 1;
235        match line {
236            SourceLine::Bpm(value) => match &mut open {
237                Some((section, _)) => section.bpm = *value,
238                None => ambient.bpm = *value,
239            },
240            SourceLine::Sig(num, den) => match &mut open {
241                Some((section, _)) => section.sig = (*num, *den),
242                None => ambient.sig = (*num, *den),
243            },
244            SourceLine::Scale(root, mode) => match &mut open {
245                Some((section, _)) => section.scale = Some((*root, *mode)),
246                None => ambient.scale = Some((*root, *mode)),
247            },
248            SourceLine::Phrase(_) => {
249                errors.push(error(
250                    at,
251                    "phrase: a piece is not edited while it plays, so it has no boundary to \
252                     land a change on — drop the line (§2.6)"
253                        .to_string(),
254                ));
255            }
256            SourceLine::Tail(seconds) => tail = *seconds,
257            SourceLine::Seed(value) => seed = *value,
258            SourceLine::Meta(key, value) => {
259                // A repeated key replaces rather than accumulates, so editing a
260                // title does not leave the old one behind in the output.
261                metadata.retain(|(existing, _)| existing != key);
262                metadata.push((key.clone(), value.clone()));
263            }
264            SourceLine::Arrange(items) => {
265                if arrangement.is_some() {
266                    errors.push(error(
267                        at,
268                        "arrange: the piece already has an arrangement — one line names the \
269                         whole order"
270                            .to_string(),
271                    ));
272                } else {
273                    arrangement = Some(items.clone());
274                }
275            }
276            SourceLine::SectionStart {
277                muted,
278                name,
279                cycles,
280            } => {
281                if sections.iter().any(|section| &section.name == name) {
282                    errors.push(error(at, format!("section '{name}' is declared twice")));
283                }
284                open = Some((
285                    Section {
286                        name: name.clone(),
287                        muted: *muted,
288                        cycles: *cycles,
289                        bpm: ambient.bpm,
290                        sig: ambient.sig,
291                        scale: ambient.scale,
292                        patterns: Vec::new(),
293                        groups: Vec::new(),
294                    },
295                    ambient,
296                ));
297            }
298            SourceLine::SectionEnd => {
299                if let Some((section, restore)) = open.take() {
300                    // A directive inside a section is scoped to it (§8.5).
301                    ambient = restore;
302                    sections.push(section);
303                }
304            }
305            SourceLine::GroupStart { muted, name } => {
306                open_group = Some((*muted, name.clone(), at));
307            }
308            SourceLine::GroupEnd(transforms) => {
309                if let Some((muted, name, _)) = open_group.take()
310                    && let Some((section, _)) = &mut open
311                {
312                    section.groups.push(GroupDef {
313                        muted,
314                        name,
315                        transforms: transforms.clone(),
316                    });
317                }
318            }
319            SourceLine::Pattern(def) => match &mut open {
320                Some((section, _)) => {
321                    if let Some(span) = def.span
322                        && let Some(reason) = span.conflict(section.cycles)
323                    {
324                        errors.push(error(
325                            at,
326                            format!("'{}': {reason} — the line can never sound", def.name),
327                        ));
328                    }
329                    if section.patterns.iter().any(|other| other.name == def.name) {
330                        errors.push(error(
331                            at,
332                            format!(
333                                "'{}' is defined twice in section '{}' — the second would \
334                                 replace the first",
335                                def.name, section.name
336                            ),
337                        ));
338                    }
339                    section.patterns.push(def.clone());
340                }
341                None => throughout.push(def.clone()),
342            },
343            _ => {}
344        }
345    }
346
347    errors.extend(check_group_chains(&sections));
348    errors.extend(check_name_collisions(&sections, &throughout));
349
350    // With no `arrange`, the arrangement is the sections in source order (§8.4).
351    let arrangement = arrangement.unwrap_or_else(|| {
352        sections
353            .iter()
354            .map(|section| ArrangeItem {
355                section: section.name.clone(),
356                repeat: 1,
357            })
358            .collect()
359    });
360
361    let (timeline, arrange_errors) = lay_out(&sections, &arrangement);
362    errors.extend(arrange_errors);
363
364    let played: Vec<&str> = timeline
365        .iter()
366        .map(|occurrence| sections[occurrence.section].name.as_str())
367        .collect();
368    let unused = sections
369        .iter()
370        .filter(|section| !played.contains(&section.name.as_str()))
371        .map(|section| section.name.clone())
372        .collect();
373
374    (
375        Piece {
376            sections,
377            arrangement,
378            timeline,
379            throughout,
380            tail,
381            seed,
382            unused,
383            metadata,
384        },
385        errors,
386    )
387}
388
389/// Turn the arrangement into a flat timeline of occurrences.
390fn lay_out(
391    sections: &[Section],
392    arrangement: &[ArrangeItem],
393) -> (Vec<Occurrence>, Vec<CompileError>) {
394    let mut timeline = Vec::new();
395    let mut errors = Vec::new();
396    let mut cycle = 0u64;
397
398    for item in arrangement {
399        let Some(index) = sections
400            .iter()
401            .position(|section| section.name == item.section)
402        else {
403            errors.push(error(
404                1,
405                format!(
406                    "arrange: '{}' is not a section in this buffer",
407                    item.section
408                ),
409            ));
410            continue;
411        };
412        for _ in 0..item.repeat {
413            timeline.push(Occurrence {
414                section: index,
415                start_cycle: cycle,
416            });
417            cycle += u64::from(sections[index].cycles);
418        }
419    }
420
421    if timeline.is_empty() && !sections.is_empty() {
422        errors.push(error(
423            1,
424            "arrange: the arrangement plays no section, so the piece has no length".to_string(),
425        ));
426    }
427
428    (timeline, errors)
429}
430
431/// A bus is one bus for the whole piece, so a group of the same name declared
432/// in two sections must declare the same chain (§7, §8.2).
433///
434/// Anything else would mean either rebuilding the bus mid-piece — audible as a
435/// reverb tail being cut — or silently picking one of the two chains.
436fn check_group_chains(sections: &[Section]) -> Vec<CompileError> {
437    let mut errors = Vec::new();
438    let mut seen: Vec<(&str, &[Transform], &str)> = Vec::new();
439
440    for section in sections {
441        for group in &section.groups {
442            match seen
443                .iter()
444                .find(|(name, _, _)| *name == group.name.as_str())
445            {
446                Some((_, chain, first)) if *chain != group.transforms.as_slice() => {
447                    errors.push(error(
448                        1,
449                        format!(
450                            "group '{}' declares a different chain in '{}' than in '{first}' — \
451                             a bus is one bus for the whole piece, so its filters cannot change \
452                             between sections",
453                            group.name, section.name
454                        ),
455                    ));
456                }
457                Some(_) => {}
458                None => seen.push((&group.name, &group.transforms, &section.name)),
459            }
460        }
461    }
462
463    errors
464}
465
466/// Names must not collide where they share a namespace.
467///
468/// Within a section, a pattern and a group would collide in the mixer, exactly
469/// as they do in a live buffer. Across sections they do not: `kick` appearing
470/// in every section is the normal way to write a piece.
471fn check_name_collisions(sections: &[Section], throughout: &[PatternDef]) -> Vec<CompileError> {
472    let mut errors = Vec::new();
473
474    for section in sections {
475        for group in &section.groups {
476            if section.patterns.iter().any(|pattern| {
477                pattern.name == group.name && pattern.group.as_deref() != Some(&group.name)
478            }) {
479                errors.push(error(
480                    1,
481                    format!(
482                        "'{}' names both a group and a pattern in section '{}'; pick two names",
483                        group.name, section.name
484                    ),
485                ));
486            }
487        }
488        if let Some(clash) = throughout.iter().find(|line| {
489            section
490                .patterns
491                .iter()
492                .any(|member| member.name == line.name)
493        }) {
494            errors.push(error(
495                1,
496                format!(
497                    "'{}' is both a line that sounds throughout the piece and a member of \
498                     section '{}' — one of them would silence the other",
499                    clash.name, section.name
500                ),
501            ));
502        }
503        if sections
504            .iter()
505            .any(|other| other.patterns.iter().any(|p| p.name == section.name))
506        {
507            errors.push(error(
508                1,
509                format!(
510                    "'{}' names both a section and a pattern; pick two names",
511                    section.name
512                ),
513            ));
514        }
515    }
516
517    errors
518}
519
520#[cfg(test)]
521mod tests;