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    /// The index into [`Piece::sections`] of the section called `name`.
137    pub fn section_index(&self, name: &str) -> Option<usize> {
138        self.sections
139            .iter()
140            .position(|section| section.name == name)
141    }
142
143    /// The absolute cycle a section first starts on, or 0 when it never plays.
144    ///
145    /// This is where a section is taken from whenever it is looked at on its
146    /// own — drawn by an editor or rendered by itself — because its generative
147    /// steps count from there.
148    pub fn first_playing(&self, section: usize) -> u64 {
149        self.timeline
150            .iter()
151            .find(|occurrence| occurrence.section == section)
152            .map_or(0, |occurrence| occurrence.start_cycle)
153    }
154
155    /// This piece cut down to one section, heard where the song first plays it
156    /// and then `passes` times back to back.
157    ///
158    /// The first occurrence keeps the section's absolute starting cycle rather
159    /// than starting from 0: choices, drops and solos count from an
160    /// occurrence's place in the piece (§3.12, §3.15, §3.19, §8.8), so a
161    /// section rendered as a fresh piece would not sound the way it does in
162    /// the song. The rule is the editor's — a section never played is taken
163    /// from cycle 0. Each further pass follows on the next cycle, exactly as
164    /// `arrange <section>*<passes>` would place it. `passes` below 1 is taken
165    /// as 1.
166    ///
167    /// Lines outside every section still sound under it, as they do in the
168    /// song, and the metadata travels so a render of the part is still filed
169    /// under the piece. Every other section is dropped, so nothing is reported
170    /// as never played.
171    pub fn isolate(&self, section: usize, passes: u32) -> Piece {
172        let kept = self.sections[section].clone();
173        let passes = passes.max(1);
174        let origin = self.first_playing(section);
175        Piece {
176            arrangement: vec![ArrangeItem {
177                section: kept.name.clone(),
178                repeat: passes,
179            }],
180            timeline: (0..passes)
181                .map(|pass| Occurrence {
182                    section: 0,
183                    start_cycle: origin + u64::from(pass) * u64::from(kept.cycles),
184                })
185                .collect(),
186            sections: vec![kept],
187            throughout: self.throughout.clone(),
188            tail: self.tail,
189            seed: self.seed,
190            unused: Vec::new(),
191            metadata: self.metadata.clone(),
192        }
193    }
194
195    /// Every instrument a line plays in the sections the arrangement plays,
196    /// or throughout them, sorted and without repeats. Muted lines count: the
197    /// instrument is written there, whether or not it sounds.
198    pub fn played_instruments(&self) -> Vec<&str> {
199        let mut played: Vec<usize> = self.timeline.iter().map(|o| o.section).collect();
200        played.sort_unstable();
201        played.dedup();
202        let mut instruments: Vec<&str> = played
203            .into_iter()
204            .flat_map(|index| &self.sections[index].patterns)
205            .chain(&self.throughout)
206            .map(|line| line.instrument.as_str())
207            .collect();
208        instruments.sort_unstable();
209        instruments.dedup();
210        instruments
211    }
212
213    /// Mute every line — in a section, in a group or throughout — whose
214    /// instrument is not one of `keep`.
215    ///
216    /// The piece keeps its length, its timing and every line's place, so what
217    /// is left sounds exactly as it does in the full mix; only the other
218    /// instruments fall silent. Groups keep their buses, so a kept line still
219    /// goes through its group's filters (§7).
220    pub fn mute_except(&mut self, keep: &[&str]) {
221        for line in self
222            .sections
223            .iter_mut()
224            .flat_map(|section| section.patterns.iter_mut())
225            .chain(self.throughout.iter_mut())
226        {
227            if !keep.contains(&line.instrument.as_str()) {
228                line.muted = true;
229            }
230        }
231    }
232
233    /// Walk the timeline, yielding each occurrence with its section and the
234    /// second it starts at.
235    ///
236    /// The start times are accumulated here rather than stored, so a `bpm`
237    /// edit inside one section cannot leave a stale offset behind on another.
238    pub fn walk(&self) -> impl Iterator<Item = PlacedSection<'_>> {
239        let mut at_seconds = 0.0;
240        self.timeline.iter().map(move |occurrence| {
241            let section = &self.sections[occurrence.section];
242            let placed = PlacedSection {
243                section,
244                start_cycle: occurrence.start_cycle,
245                start_seconds: at_seconds,
246            };
247            at_seconds += section.seconds();
248            placed
249        })
250    }
251}
252
253/// One occurrence, resolved to its section and its position in real time.
254#[derive(Debug, Clone, Copy, PartialEq)]
255pub struct PlacedSection<'a> {
256    pub section: &'a Section,
257    /// Absolute cycle the occurrence starts on, counted from 0.
258    pub start_cycle: u64,
259    /// Absolute time the occurrence starts at, in seconds.
260    pub start_seconds: f64,
261}
262
263/// How long one cycle lasts at `bpm` in `signature`, in seconds.
264pub fn cycle_seconds(bpm: u32, signature: (u8, u8)) -> f64 {
265    let quarters = f64::from(signature.0) * 4.0 / f64::from(signature.1.max(1));
266    quarters * 60.0 / f64::from(bpm.max(1))
267}
268
269/// Whether a program is a piece: it has at least one `section` (§8.1).
270pub fn is_piece(program: &Program) -> bool {
271    program
272        .lines
273        .iter()
274        .any(|line| matches!(line, SourceLine::SectionStart { .. }))
275}
276
277fn error(line: usize, message: String) -> CompileError {
278    CompileError {
279        kind: CompileErrorKind::ParseError,
280        location: SourceLocation {
281            line,
282            column: 1,
283            file: None,
284        },
285        message,
286        suggestion: None,
287    }
288}
289
290/// State carried down the buffer while resolving, so a section can inherit the
291/// values in force above it and restore them when it ends (§8.5).
292#[derive(Clone, Copy)]
293struct Ambient {
294    bpm: u32,
295    sig: (u8, u8),
296    scale: Option<(PitchRoot, ScaleMode)>,
297}
298
299/// Resolve a parsed program into a [`Piece`].
300///
301/// `defaults` is the state a buffer starts in — the session's own `bpm`, `sig`
302/// and `scale` — which top-level directives then override.
303///
304/// Returns the piece alongside any errors. A piece is returned even when the
305/// errors are non-empty so a consumer can still show the structure it did
306/// understand; whether to play it is the consumer's call.
307pub fn resolve(
308    program: &Program,
309    defaults: (u32, (u8, u8), Option<(PitchRoot, ScaleMode)>),
310) -> (Piece, Vec<CompileError>) {
311    let mut errors = Vec::new();
312    let mut ambient = Ambient {
313        bpm: defaults.0,
314        sig: defaults.1,
315        scale: defaults.2,
316    };
317
318    let mut sections: Vec<Section> = Vec::new();
319    let mut throughout: Vec<PatternDef> = Vec::new();
320    let mut arrangement: Option<Vec<ArrangeItem>> = None;
321    let mut tail = DEFAULT_TAIL;
322    let mut seed = 0u64;
323    let mut metadata: Vec<(String, String)> = Vec::new();
324
325    // The section being filled, and the ambient state to restore when it ends.
326    let mut open: Option<(Section, Ambient)> = None;
327    // The group being filled inside the current section.
328    let mut open_group: Option<(bool, String, usize)> = None;
329
330    for (index, line) in program.lines.iter().enumerate() {
331        let at = index + 1;
332        match line {
333            SourceLine::Bpm(value) => match &mut open {
334                Some((section, _)) => section.bpm = *value,
335                None => ambient.bpm = *value,
336            },
337            SourceLine::Sig(num, den) => match &mut open {
338                Some((section, _)) => section.sig = (*num, *den),
339                None => ambient.sig = (*num, *den),
340            },
341            SourceLine::Scale(root, mode) => match &mut open {
342                Some((section, _)) => section.scale = Some((*root, *mode)),
343                None => ambient.scale = Some((*root, *mode)),
344            },
345            SourceLine::Phrase(_) => {
346                errors.push(error(
347                    at,
348                    "phrase: a piece is not edited while it plays, so it has no boundary to \
349                     land a change on — drop the line (§2.6)"
350                        .to_string(),
351                ));
352            }
353            SourceLine::Tail(seconds) => tail = *seconds,
354            SourceLine::Seed(value) => seed = *value,
355            SourceLine::Meta(key, value) => {
356                // A repeated key replaces rather than accumulates, so editing a
357                // title does not leave the old one behind in the output.
358                metadata.retain(|(existing, _)| existing != key);
359                metadata.push((key.clone(), value.clone()));
360            }
361            SourceLine::Arrange(items) => {
362                if arrangement.is_some() {
363                    errors.push(error(
364                        at,
365                        "arrange: the piece already has an arrangement — one line names the \
366                         whole order"
367                            .to_string(),
368                    ));
369                } else {
370                    arrangement = Some(items.clone());
371                }
372            }
373            SourceLine::SectionStart {
374                muted,
375                name,
376                cycles,
377            } => {
378                if sections.iter().any(|section| &section.name == name) {
379                    errors.push(error(at, format!("section '{name}' is declared twice")));
380                }
381                open = Some((
382                    Section {
383                        name: name.clone(),
384                        muted: *muted,
385                        cycles: *cycles,
386                        bpm: ambient.bpm,
387                        sig: ambient.sig,
388                        scale: ambient.scale,
389                        patterns: Vec::new(),
390                        groups: Vec::new(),
391                    },
392                    ambient,
393                ));
394            }
395            SourceLine::SectionEnd => {
396                if let Some((section, restore)) = open.take() {
397                    // A directive inside a section is scoped to it (§8.5).
398                    ambient = restore;
399                    sections.push(section);
400                }
401            }
402            SourceLine::GroupStart { muted, name } => {
403                open_group = Some((*muted, name.clone(), at));
404            }
405            SourceLine::GroupEnd(transforms) => {
406                if let Some((muted, name, _)) = open_group.take()
407                    && let Some((section, _)) = &mut open
408                {
409                    section.groups.push(GroupDef {
410                        muted,
411                        name,
412                        transforms: transforms.clone(),
413                    });
414                }
415            }
416            SourceLine::Pattern(def) => match &mut open {
417                Some((section, _)) => {
418                    if let Some(span) = def.span
419                        && let Some(reason) = span.conflict(section.cycles)
420                    {
421                        errors.push(error(
422                            at,
423                            format!("'{}': {reason} — the line can never sound", def.name),
424                        ));
425                    }
426                    if section.patterns.iter().any(|other| other.name == def.name) {
427                        errors.push(error(
428                            at,
429                            format!(
430                                "'{}' is defined twice in section '{}' — the second would \
431                                 replace the first",
432                                def.name, section.name
433                            ),
434                        ));
435                    }
436                    section.patterns.push(def.clone());
437                }
438                None => throughout.push(def.clone()),
439            },
440            _ => {}
441        }
442    }
443
444    errors.extend(check_group_chains(&sections));
445    errors.extend(check_name_collisions(&sections, &throughout));
446
447    // With no `arrange`, the arrangement is the sections in source order (§8.4).
448    let arrangement = arrangement.unwrap_or_else(|| {
449        sections
450            .iter()
451            .map(|section| ArrangeItem {
452                section: section.name.clone(),
453                repeat: 1,
454            })
455            .collect()
456    });
457
458    let (timeline, arrange_errors) = lay_out(&sections, &arrangement);
459    errors.extend(arrange_errors);
460
461    let played: Vec<&str> = timeline
462        .iter()
463        .map(|occurrence| sections[occurrence.section].name.as_str())
464        .collect();
465    let unused = sections
466        .iter()
467        .filter(|section| !played.contains(&section.name.as_str()))
468        .map(|section| section.name.clone())
469        .collect();
470
471    (
472        Piece {
473            sections,
474            arrangement,
475            timeline,
476            throughout,
477            tail,
478            seed,
479            unused,
480            metadata,
481        },
482        errors,
483    )
484}
485
486/// Turn the arrangement into a flat timeline of occurrences.
487fn lay_out(
488    sections: &[Section],
489    arrangement: &[ArrangeItem],
490) -> (Vec<Occurrence>, Vec<CompileError>) {
491    let mut timeline = Vec::new();
492    let mut errors = Vec::new();
493    let mut cycle = 0u64;
494
495    for item in arrangement {
496        let Some(index) = sections
497            .iter()
498            .position(|section| section.name == item.section)
499        else {
500            errors.push(error(
501                1,
502                format!(
503                    "arrange: '{}' is not a section in this buffer",
504                    item.section
505                ),
506            ));
507            continue;
508        };
509        for _ in 0..item.repeat {
510            timeline.push(Occurrence {
511                section: index,
512                start_cycle: cycle,
513            });
514            cycle += u64::from(sections[index].cycles);
515        }
516    }
517
518    if timeline.is_empty() && !sections.is_empty() {
519        errors.push(error(
520            1,
521            "arrange: the arrangement plays no section, so the piece has no length".to_string(),
522        ));
523    }
524
525    (timeline, errors)
526}
527
528/// A bus is one bus for the whole piece, so a group of the same name declared
529/// in two sections must declare the same chain (§7, §8.2).
530///
531/// Anything else would mean either rebuilding the bus mid-piece — audible as a
532/// reverb tail being cut — or silently picking one of the two chains.
533fn check_group_chains(sections: &[Section]) -> Vec<CompileError> {
534    let mut errors = Vec::new();
535    let mut seen: Vec<(&str, &[Transform], &str)> = Vec::new();
536
537    for section in sections {
538        for group in &section.groups {
539            match seen
540                .iter()
541                .find(|(name, _, _)| *name == group.name.as_str())
542            {
543                Some((_, chain, first)) if *chain != group.transforms.as_slice() => {
544                    errors.push(error(
545                        1,
546                        format!(
547                            "group '{}' declares a different chain in '{}' than in '{first}' — \
548                             a bus is one bus for the whole piece, so its filters cannot change \
549                             between sections",
550                            group.name, section.name
551                        ),
552                    ));
553                }
554                Some(_) => {}
555                None => seen.push((&group.name, &group.transforms, &section.name)),
556            }
557        }
558    }
559
560    errors
561}
562
563/// Names must not collide where they share a namespace.
564///
565/// Within a section, a pattern and a group would collide in the mixer, exactly
566/// as they do in a live buffer. Across sections they do not: `kick` appearing
567/// in every section is the normal way to write a piece.
568fn check_name_collisions(sections: &[Section], throughout: &[PatternDef]) -> Vec<CompileError> {
569    let mut errors = Vec::new();
570
571    for section in sections {
572        for group in &section.groups {
573            if section.patterns.iter().any(|pattern| {
574                pattern.name == group.name && pattern.group.as_deref() != Some(&group.name)
575            }) {
576                errors.push(error(
577                    1,
578                    format!(
579                        "'{}' names both a group and a pattern in section '{}'; pick two names",
580                        group.name, section.name
581                    ),
582                ));
583            }
584        }
585        if let Some(clash) = throughout.iter().find(|line| {
586            section
587                .patterns
588                .iter()
589                .any(|member| member.name == line.name)
590        }) {
591            errors.push(error(
592                1,
593                format!(
594                    "'{}' is both a line that sounds throughout the piece and a member of \
595                     section '{}' — one of them would silence the other",
596                    clash.name, section.name
597                ),
598            ));
599        }
600        if sections
601            .iter()
602            .any(|other| other.patterns.iter().any(|p| p.name == section.name))
603        {
604            errors.push(error(
605                1,
606                format!(
607                    "'{}' names both a section and a pattern; pick two names",
608                    section.name
609                ),
610            ));
611        }
612    }
613
614    errors
615}
616
617#[cfg(test)]
618mod tests;